diff --git a/CHANGELOG.md b/CHANGELOG.md
index 628843b59..05cf9c0a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to solstone (the Python package) will be documented in this
Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), aligned with `cmo/brand/changelog-voice.md`.
+## [0.6.5] - 2026-06-16
+
+### Changed
+- the local web interface no longer has its own password, login page, session cookie, or localhost-trust switch. once setup is complete, the local interface serves directly on the journal machine; linked devices continue to use their paired-device identity through link.
+
## [0.6.4] - 2026-06-15
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 44b05c5ad..562956004 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,13 +66,12 @@ journal setup
The source-checkout journal lives at `journal/` inside the repo unless you pass `--journal` or have already configured another path.
-Configure API keys and the web password in `journal/config/journal.json`. This file is the only key configuration method for source-checkout development:
+Configure API keys in `journal/config/journal.json`. This file is the only key configuration method for source-checkout development:
```bash
mkdir -p journal/config
cat > journal/config/journal.json << 'EOF'
{
- "convey": {},
"env": {
"GOOGLE_API_KEY": "your-key-here"
}
@@ -81,11 +80,10 @@ EOF
chmod 600 journal/config/journal.json
```
-Run `journal password set` to configure web authentication. Replace `your-key-here` with your Google AI API key. Optional provider keys can be added to the same `env` object:
+Replace `your-key-here` with your Google AI API key. Optional provider keys can be added to the same `env` object:
```json
{
- "convey": {},
"env": {
"GOOGLE_API_KEY": "your-gemini-key",
"OPENAI_API_KEY": "your-openai-key",
diff --git a/INSTALL.md b/INSTALL.md
index 612a162be..ce7729392 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -63,7 +63,7 @@ journal setup
this runs the setup readiness doctor battery, confirms the journal directory at `~/journal`, installs the local transcription model (~2.5 GB on linux), installs the `sol` skill for claude code, codex, and gemini, installs the journal-side `sol` and `journal` router skills so sol can tend the journal, and starts a background service (systemd on linux, launchd on macOS) listening on http://localhost:5015.
-let your human know: **open http://localhost:5015 in a browser**. the first-run wizard walks them through setting their identity and connecting a gemini API key. an optional password can be configured later in settings → security.
+let your human know: **open http://localhost:5015 in a browser**. the first-run wizard walks them through setting their identity and connecting a gemini API key.
a `solstone[journal]` install bundles everything a journal host needs — PDF rendering, whisper, and the default CPU transcription stack are all included; `journal setup` downloads the transcription model. there are no separate à-la-carte extras to add. if the readiness doctor step (`sol doctor --readiness`) finds missing system libraries, it will tell you the exact install command to run for your platform.
diff --git a/README.md b/README.md
index 7d110b025..d69c4d9a0 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ journal setup
want only the thin `sol` client — to talk to a journal running elsewhere? `uv tool install solstone` (no extras), or `uvx solstone` for an ephemeral one-shot.
-then open http://localhost:5015 in a browser; the first-run wizard handles identity and the gemini API key. an optional password can be configured later in settings → security.
+then open http://localhost:5015 in a browser; the first-run wizard handles identity and the gemini API key.
see [INSTALL.md](INSTALL.md) for prerequisites, observer install, and troubleshooting; see [CONTRIBUTING.md](CONTRIBUTING.md) if you want to develop on solstone from a source checkout.
diff --git a/docs/CONVEY.md b/docs/CONVEY.md
index 2b290cf4a..2da4daa30 100644
--- a/docs/CONVEY.md
+++ b/docs/CONVEY.md
@@ -16,19 +16,9 @@ Run the server with:
convey
```
-### Authentication
-
-Password authentication is configured from Settings → Security. For headless setups, use the CLI:
-
-```bash
-journal password set
-```
-
-When a password is set, it is stored as a secure hash in `config/journal.json` under `convey.password_hash`.
-
## Architecture
-Convey uses an **app plugin system** where all functional views are implemented as independent apps in the `/apps/` directory. The core `solstone/convey/` package provides authentication, WebSocket communication, and the app loading infrastructure.
+Convey uses an **app plugin system** where all functional views are implemented as independent apps in the `/apps/` directory. The core `solstone/convey/` package provides access gating, WebSocket communication, and the app loading infrastructure.
```
convey/
@@ -36,14 +26,11 @@ convey/
state.py - global state (journal_root)
bridge.py - Callosum WebSocket bridge for real-time events
utils.py - shared helpers (format_date, spawn_agent, etc.)
- views/
- __init__.py - blueprint registration
- home.py - authentication (login/logout) and root redirect
+ root.py - access gate, setup routes, and root redirect
templates/
app.html - main app container template
menu_bar.html - dynamic left sidebar menu
status_pane.html - WebSocket status indicator
- login.html - login page
macros.html - Jinja macros
static/ - shared CSS and JavaScript
app.css - app system styles
@@ -73,11 +60,9 @@ Browse `/apps/` to see available apps.
### Core Routes
-The `solstone/convey/views/home.py` module provides essential routes:
+The `solstone/convey/root.py` module provides essential routes:
- `/` - Redirects to `/app/home/`
-- `/login` - Authentication page
-- `/logout` - Clear session and redirect to login
- `/favicon.ico` - Serve favicon
All functional views are accessed at `/app/{name}/` URLs.
@@ -123,7 +108,7 @@ from a client-supplied field. Key-based ingest authenticates with an
`Authorization: Bearer` header — never a key in the URL path — and derives its
storage scope from the authenticated record; a scope id in the URL is an assertion
to check against that record, not an input. Any key-authenticated route must be in
-the `require_login` allowlist.
+the `require_access` allowlist.
**Pagination.** Use `parse_pagination_params()` (offset/limit, max 100) or a cursor;
no list endpoint returns an unbounded full array.
diff --git a/docs/design/providers-panel-consolidation.md b/docs/design/providers-panel-consolidation.md
index e5d615e22..b90ad5a8b 100644
--- a/docs/design/providers-panel-consolidation.md
+++ b/docs/design/providers-panel-consolidation.md
@@ -181,7 +181,7 @@ Do not add persisted local active-model state in this lode. That would be a sele
| Endpoint payload | New `solstone/apps/settings/tests/test_providers_payload_extended.py` | Use the `settings_client` pattern from `test_workspace_install_copy_template.py:15-26`; `GET /api/providers`; assert top-level `local` and `mlx` dicts exist; each includes the 7 `InstallStatus` fields and `install_state` is in the canonical vocabulary from `InstallState` (`install_state.py:11-19`). |
| Visual smoke | New `solstone/apps/settings/tests/test_providers_panel_visual.py` | Mirror the werkzeug + pytest-playwright pattern in `test_workspace_qr_size.py:22-52`; navigate to `/app/settings/`; assert one panel renders five cards for `anthropic`, `openai`, `openhands`, `local`, `mlx`; assert each card badge text is from `INSTALL_COPY`; assert retired DOM ids are absent. Mark `@pytest.mark.integration`. |
-Visual smoke data source: use a clean temporary settings journal with `setup.completed_at` and `convey.trust_localhost`, following `settings_client` (`test_workspace_install_copy_template.py:15-26`) rather than monkeypatching provider state. Clean fixture is preferred because it exercises the real route and template path.
+Visual smoke data source: use a clean temporary settings journal with `setup.completed_at`, following `settings_client` (`test_workspace_install_copy_template.py:15-26`) rather than monkeypatching provider state. Clean fixture is preferred because it exercises the real route and template path.
Playwright precondition: `pytest-playwright` is already in dev dependencies (`pyproject.toml:181-189`), and `make install` installs Chromium (`Makefile:54-58`). Running the visual smoke outside the installed dev env requires the same `playwright install chromium` precondition.
diff --git a/solstone/apps/activities/tests/conftest.py b/solstone/apps/activities/tests/conftest.py
index c81ea9c2d..ba1ad7e9a 100644
--- a/solstone/apps/activities/tests/conftest.py
+++ b/solstone/apps/activities/tests/conftest.py
@@ -28,7 +28,6 @@ def activities_env(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
diff --git a/solstone/apps/activities/tests/test_routes.py b/solstone/apps/activities/tests/test_routes.py
index 40e78768f..c651a9cfb 100644
--- a/solstone/apps/activities/tests/test_routes.py
+++ b/solstone/apps/activities/tests/test_routes.py
@@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parents[4]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
def test_day_activities_returns_collection_envelope(activities_env):
@@ -50,7 +50,7 @@ def test_day_activities_empty_day_returns_empty_envelope(activities_env):
def test_create_record_rejects_empty_title(activities_env):
journal, facet, day, _day_path = activities_env(None)
- client = make_logged_in_test_client(journal)
+ client = make_test_client(journal)
response = client.post(
f"/app/activities/api/day/{day}/records?facet={facet}",
@@ -63,7 +63,7 @@ def test_create_record_rejects_empty_title(activities_env):
def test_create_record_rejects_invalid_source(activities_env):
journal, facet, day, _day_path = activities_env(None)
- client = make_logged_in_test_client(journal)
+ client = make_test_client(journal)
response = client.post(
f"/app/activities/api/day/{day}/records?facet={facet}",
diff --git a/solstone/apps/backup/tests/conftest.py b/solstone/apps/backup/tests/conftest.py
index a021d9199..af87ad577 100644
--- a/solstone/apps/backup/tests/conftest.py
+++ b/solstone/apps/backup/tests/conftest.py
@@ -41,7 +41,6 @@ def backup_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
@@ -55,9 +54,6 @@ def backup_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
app = create_app(journal=str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return Env(journal=journal, client=client, app=app)
return _create
diff --git a/solstone/apps/backup/tests/test_flows.py b/solstone/apps/backup/tests/test_flows.py
index 2caa99a07..6778ee16e 100644
--- a/solstone/apps/backup/tests/test_flows.py
+++ b/solstone/apps/backup/tests/test_flows.py
@@ -21,6 +21,7 @@ def _config_path(env) -> Path:
def _write_config(env, payload: dict) -> None:
+ payload.setdefault("setup", {"completed_at": 1700000000000})
_config_path(env).write_text(json.dumps(payload), encoding="utf-8")
diff --git a/solstone/apps/chat/tests/test_call.py b/solstone/apps/chat/tests/test_call.py
index 1ccaefe95..025a7a2fe 100644
--- a/solstone/apps/chat/tests/test_call.py
+++ b/solstone/apps/chat/tests/test_call.py
@@ -20,7 +20,7 @@ import solstone.convey.sol_initiated.start as sol_start
from solstone.apps.chat.call import app
from solstone.convey.chat_stream import read_chat_events
from solstone.think.convey_client import ConveyClient
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
FROZEN_MS = 1_700_000_000_000
FROZEN_DAY = datetime.fromtimestamp(FROZEN_MS / 1000).strftime("%Y%m%d")
@@ -33,7 +33,13 @@ def journal(tmp_path, monkeypatch):
config_path = tmp_path / "config" / "journal.json"
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(
- json.dumps({"sol_voice": {"rate_floor_minutes": 0}}) + "\n",
+ json.dumps(
+ {
+ "setup": {"completed_at": 1700000000000},
+ "sol_voice": {"rate_floor_minutes": 0},
+ }
+ )
+ + "\n",
encoding="utf-8",
)
return tmp_path
@@ -42,7 +48,7 @@ def journal(tmp_path, monkeypatch):
@pytest.fixture
def runner(journal, monkeypatch):
monkeypatch.setattr(sol_start, "now_ms", lambda: FROZEN_MS)
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.chat.call.get_client", lambda: client)
return CliRunner()
diff --git a/solstone/apps/chat/tests/test_liveness_chat_bar.py b/solstone/apps/chat/tests/test_liveness_chat_bar.py
index 9252a0a02..d1d7894a0 100644
--- a/solstone/apps/chat/tests/test_liveness_chat_bar.py
+++ b/solstone/apps/chat/tests/test_liveness_chat_bar.py
@@ -20,7 +20,6 @@ def chat_client(tmp_path, monkeypatch):
json.dumps(
{
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
)
+ "\n",
@@ -30,9 +29,6 @@ def chat_client(tmp_path, monkeypatch):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/solstone/apps/chat/tests/test_liveness_live.py b/solstone/apps/chat/tests/test_liveness_live.py
index 4fbfc081a..00d67fb77 100644
--- a/solstone/apps/chat/tests/test_liveness_live.py
+++ b/solstone/apps/chat/tests/test_liveness_live.py
@@ -19,7 +19,6 @@ def chat_client(tmp_path, monkeypatch):
json.dumps(
{
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
)
+ "\n",
@@ -29,9 +28,6 @@ def chat_client(tmp_path, monkeypatch):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/solstone/apps/chat/tests/test_origin_tag_live.py b/solstone/apps/chat/tests/test_origin_tag_live.py
index bfa5cb318..5f10d1408 100644
--- a/solstone/apps/chat/tests/test_origin_tag_live.py
+++ b/solstone/apps/chat/tests/test_origin_tag_live.py
@@ -25,7 +25,6 @@ def chat_client(tmp_path, monkeypatch):
json.dumps(
{
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
)
+ "\n",
@@ -35,9 +34,6 @@ def chat_client(tmp_path, monkeypatch):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/solstone/apps/chat/tests/test_origin_tag_ssr.py b/solstone/apps/chat/tests/test_origin_tag_ssr.py
index fd1f52cf8..e78a36b56 100644
--- a/solstone/apps/chat/tests/test_origin_tag_ssr.py
+++ b/solstone/apps/chat/tests/test_origin_tag_ssr.py
@@ -34,7 +34,6 @@ def chat_env(tmp_path, monkeypatch):
json.dumps(
{
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
"identity": {"preferred": "Owner"},
"agent": {"name": "sol"},
}
@@ -46,9 +45,6 @@ def chat_env(tmp_path, monkeypatch):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client, journal
diff --git a/solstone/apps/chat/tests/test_routes.py b/solstone/apps/chat/tests/test_routes.py
index b309b2fd4..a3f0deb87 100644
--- a/solstone/apps/chat/tests/test_routes.py
+++ b/solstone/apps/chat/tests/test_routes.py
@@ -49,9 +49,6 @@ def _make_env(journal, monkeypatch) -> ChatTestEnv:
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return ChatTestEnv(client=client, journal=journal)
diff --git a/solstone/apps/curation/tests/conftest.py b/solstone/apps/curation/tests/conftest.py
index 204e2c41b..b3cb2f16c 100644
--- a/solstone/apps/curation/tests/conftest.py
+++ b/solstone/apps/curation/tests/conftest.py
@@ -24,7 +24,6 @@ def curation_env(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/entities/tests/test_delete_journal_entity.py b/solstone/apps/entities/tests/test_delete_journal_entity.py
index bd4df28a0..a333bf759 100644
--- a/solstone/apps/entities/tests/test_delete_journal_entity.py
+++ b/solstone/apps/entities/tests/test_delete_journal_entity.py
@@ -111,7 +111,7 @@ def test_cancel_delete_journal_entity_within_window_keeps_entity(
):
entity_id = "cancel-delete-test"
_create_journal_entity(entity_id)
- monkeypatch.setattr("solstone.apps.entities.routes.ENTITY_DELETE_TTL", 0.2)
+ monkeypatch.setattr("solstone.apps.entities.routes.ENTITY_DELETE_TTL", 1.0)
delete_response = client.delete(f"/app/entities/api/journal/entity/{entity_id}")
pending_id = delete_response.get_json()["pending"]
diff --git a/solstone/apps/entities/tests/test_merge.py b/solstone/apps/entities/tests/test_merge.py
index 808bf4073..c2594928e 100644
--- a/solstone/apps/entities/tests/test_merge.py
+++ b/solstone/apps/entities/tests/test_merge.py
@@ -17,7 +17,7 @@ from solstone.apps.entities.call import app as entities_app
from solstone.think.convey_client import ConveyClient
from solstone.think.entities import merge as merge_mod
from solstone.think.entities.journal import load_journal_entity
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
runner = CliRunner()
STREAM = "test"
@@ -28,7 +28,7 @@ def _entities_client(monkeypatch: pytest.MonkeyPatch) -> None:
def client() -> ConveyClient:
journal = Path(os.environ["SOLSTONE_JOURNAL"])
return ConveyClient(
- session=make_logged_in_test_client(journal),
+ session=make_test_client(journal),
base_url="",
)
diff --git a/solstone/apps/facets/tests/test_call.py b/solstone/apps/facets/tests/test_call.py
index 2c13ef2bf..e0f99a5dc 100644
--- a/solstone/apps/facets/tests/test_call.py
+++ b/solstone/apps/facets/tests/test_call.py
@@ -23,18 +23,19 @@ from solstone.think.facet_review_candidates import (
record_facet_candidate,
)
from solstone.think.journal_io import LockTimeout
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.facets.call.get_client", lambda: client)
return CliRunner()
diff --git a/solstone/apps/health/tests/conftest.py b/solstone/apps/health/tests/conftest.py
index 0704b33ae..9785bfaf9 100644
--- a/solstone/apps/health/tests/conftest.py
+++ b/solstone/apps/health/tests/conftest.py
@@ -27,7 +27,6 @@ def health_env(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/link/copy.py b/solstone/apps/link/copy.py
index 638bd20ec..87f95c430 100644
--- a/solstone/apps/link/copy.py
+++ b/solstone/apps/link/copy.py
@@ -50,8 +50,6 @@ SUCCESS_VERIFY_NOTE_ANYWHERE = (
"fingerprint should match what it shows. didn't do this?"
)
SUCCESS_REMOVE_LABEL = "that wasn't me — remove"
-PAIR_WEB_PASSWORD_SETTINGS_LINK = "set a web password for this page in settings →"
-
# --- U4 first-run hero ---
HERO_TITLE = "let's connect a device"
HERO_BODY = (
diff --git a/solstone/apps/link/routes.py b/solstone/apps/link/routes.py
index 9a1984136..12514e62c 100644
--- a/solstone/apps/link/routes.py
+++ b/solstone/apps/link/routes.py
@@ -115,7 +115,7 @@ from solstone.think.pairing.config import (
)
from solstone.think.services import operations, spl, spl_handoff
from solstone.think.services import status as service_status
-from solstone.think.utils import get_config, get_journal, now_ms
+from solstone.think.utils import get_journal, now_ms
logger = logging.getLogger(__name__)
MANUAL_CODE_RE = re.compile(rf"^[0-9A-HJKMNP-TV-Z]{{{MANUAL_CODE_LEN}}}$")
@@ -173,11 +173,6 @@ def _is_loopback_request() -> bool:
return request.remote_addr in {"127.0.0.1", "::1"}
-def _convey_password_is_set() -> bool:
- password_hash = get_config().get("convey", {}).get("password_hash", "")
- return bool(str(password_hash or "").strip())
-
-
def _read_link_connection_event() -> str | None:
event = get_cached_state().get("link_connection")
return event if isinstance(event, str) else None
@@ -404,7 +399,6 @@ def api_status() -> Any:
"enrolled": token_present,
"relay_url": relay_url(),
"ca_fingerprint": ca_fp,
- "has_password": _convey_password_is_set(),
"lan_accessible": lan_accessible,
"posture": posture,
"reachability": reachability,
diff --git a/solstone/apps/link/tests/conftest.py b/solstone/apps/link/tests/conftest.py
index b691b2612..8329f9da7 100644
--- a/solstone/apps/link/tests/conftest.py
+++ b/solstone/apps/link/tests/conftest.py
@@ -27,7 +27,6 @@ def link_env(tmp_path, monkeypatch):
config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "journal.json"
config = {
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
if posture is not None:
diff --git a/solstone/apps/link/tests/test_api_status.py b/solstone/apps/link/tests/test_api_status.py
index 083d473ac..b98c07b62 100644
--- a/solstone/apps/link/tests/test_api_status.py
+++ b/solstone/apps/link/tests/test_api_status.py
@@ -21,7 +21,6 @@ TOTP_SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
def _write_config(env: Any, *, link: Any = None, include_link: bool = True) -> None:
config: dict[str, Any] = {
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
if include_link:
@@ -324,7 +323,6 @@ def test_back_compat_field_set(link_env, monkeypatch) -> None:
"enrolled",
"relay_url",
"ca_fingerprint",
- "has_password",
"lan_accessible",
"posture",
"reachability",
@@ -337,7 +335,6 @@ def test_back_compat_field_set(link_env, monkeypatch) -> None:
assert isinstance(data["enrolled"], bool)
assert isinstance(data["relay_url"], str)
assert isinstance(data["ca_fingerprint"], str) or data["ca_fingerprint"] is None
- assert isinstance(data["has_password"], bool)
assert isinstance(data["lan_accessible"], bool)
@@ -359,7 +356,6 @@ def test_api_status_unprovisioned(link_env, monkeypatch) -> None:
"enrolled",
"relay_url",
"ca_fingerprint",
- "has_password",
"lan_accessible",
"posture",
"reachability",
@@ -396,7 +392,6 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) ->
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
@@ -407,9 +402,6 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) ->
app = create_app(journal=str(journal))
app.config["TESTING"] = True
test_client = app.test_client()
- with test_client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
client = ConveyClient(session=test_client, base_url="")
monkeypatch.setattr(link_call, "get_client", lambda: client)
@@ -426,19 +418,3 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) ->
)
assert "Home label: (not provisioned)" in result.stdout
assert not (journal / "link" / "state.json").exists()
-
-
-def test_status_reports_convey_password_state(link_env, monkeypatch) -> None:
- env = link_env()
- monkeypatch.setattr(link_routes, "_detect_lan_ip", lambda: "192.168.1.50")
-
- data = _get_status(env)
- assert data["has_password"] is False
-
- config_path = env.journal / "config" / "journal.json"
- config = json.loads(config_path.read_text("utf-8"))
- config.setdefault("convey", {})["password_hash"] = "hashed"
- config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
-
- data = _get_status(env)
- assert data["has_password"] is True
diff --git a/solstone/apps/link/tests/test_reach_copy.py b/solstone/apps/link/tests/test_reach_copy.py
index ab7aa85fd..97a9ce160 100644
--- a/solstone/apps/link/tests/test_reach_copy.py
+++ b/solstone/apps/link/tests/test_reach_copy.py
@@ -147,13 +147,6 @@ def test_reach_shell_corrected_copy_is_locked() -> None:
assert copy.PRIVATE_LINK_RETRY_CTA == "try again"
-def test_pair_web_password_settings_link_is_locked() -> None:
- assert (
- copy.PAIR_WEB_PASSWORD_SETTINGS_LINK
- == "set a web password for this page in settings →"
- )
-
-
def test_reach_shell_copy_stays_in_bounds() -> None:
banned_terms = (
"sign in",
@@ -174,7 +167,6 @@ def test_reach_shell_copy_stays_in_bounds() -> None:
for value in [
*U2_COPY_VALUES,
- copy.PAIR_WEB_PASSWORD_SETTINGS_LINK,
]:
lowered = value.lower()
for term in banned_terms:
diff --git a/solstone/apps/link/tests/test_workspace_copy.py b/solstone/apps/link/tests/test_workspace_copy.py
index 34cf255d9..05489e853 100644
--- a/solstone/apps/link/tests/test_workspace_copy.py
+++ b/solstone/apps/link/tests/test_workspace_copy.py
@@ -29,7 +29,6 @@ MODAL_COPY_VALUES = [
copy.SUCCESS_DONE,
copy.SUCCESS_VERIFY_NOTE,
copy.SUCCESS_REMOVE_LABEL,
- copy.PAIR_WEB_PASSWORD_SETTINGS_LINK,
]
diff --git a/solstone/apps/link/workspace.html b/solstone/apps/link/workspace.html
index d45f2305c..80f7d6c21 100644
--- a/solstone/apps/link/workspace.html
+++ b/solstone/apps/link/workspace.html
@@ -228,7 +228,6 @@
{{ link_copy.REACH_HOME_ADDRESS_LABEL }}
- {{ link_copy.PAIR_WEB_PASSWORD_SETTINGS_LINK }}
diff --git a/solstone/apps/observer/routes.py b/solstone/apps/observer/routes.py
index 9cc7bd4dc..6c28b277b 100644
--- a/solstone/apps/observer/routes.py
+++ b/solstone/apps/observer/routes.py
@@ -372,7 +372,7 @@ _REGISTER_REQUIRED_FIELDS = ("platform", "hostname", "stream_type", "version")
def _is_trusted_localhost() -> bool:
- """Direct-loopback check mirroring the convey trust_localhost gate."""
+ """Direct-loopback check for observer-local endpoints."""
is_localhost = request.remote_addr in ("127.0.0.1", "::1", "localhost")
proxy_headers = (
request.headers.get("X-Forwarded-For")
@@ -402,7 +402,7 @@ def register() -> Any:
"""Self-register a local or through-link observer and lock stream identity.
The in-handler guard is the sole gate: direct loopback, or a request that
- arrived through an authorized PL identity. The route is require_login-exempt
+ arrived through an authorized PL identity. The route is require_access-exempt
so an observer can register before setup completes. Mints the DL handle,
locks a stream onto the record, and returns the pinned descriptor response.
"""
diff --git a/solstone/apps/observer/tests/conftest.py b/solstone/apps/observer/tests/conftest.py
index decaf0696..2fdb35160 100644
--- a/solstone/apps/observer/tests/conftest.py
+++ b/solstone/apps/observer/tests/conftest.py
@@ -32,7 +32,6 @@ def observer_env(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/observer/tests/test_call.py b/solstone/apps/observer/tests/test_call.py
index e28891b12..156791f3f 100644
--- a/solstone/apps/observer/tests/test_call.py
+++ b/solstone/apps/observer/tests/test_call.py
@@ -20,18 +20,19 @@ if str(ROOT) not in sys.path:
from solstone.think.call import call_app
from solstone.think.convey_client import ConveyClient
from solstone.think.streams import update_stream, write_segment_stream
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.observer.call.get_client", lambda: client)
return CliRunner()
diff --git a/solstone/apps/observer/tests/test_pl_pairing.py b/solstone/apps/observer/tests/test_pl_pairing.py
index ba463f8e1..9473e9a4a 100644
--- a/solstone/apps/observer/tests/test_pl_pairing.py
+++ b/solstone/apps/observer/tests/test_pl_pairing.py
@@ -31,7 +31,6 @@ def pair_env(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/observer/tests/test_register.py b/solstone/apps/observer/tests/test_register.py
index 6fcdfaf6c..290236884 100644
--- a/solstone/apps/observer/tests/test_register.py
+++ b/solstone/apps/observer/tests/test_register.py
@@ -163,8 +163,6 @@ def test_register_works_before_setup_complete(tmp_path, monkeypatch):
config_path = journal_copy / "config" / "journal.json"
config = json.loads(config_path.read_text())
- config.setdefault("convey", {}).pop("password_hash", None)
- config["convey"].pop("password", None)
config.setdefault("setup", {}).pop("completed_at", None)
config_path.write_text(json.dumps(config, indent=2))
diff --git a/solstone/apps/observer/tests/test_routes.py b/solstone/apps/observer/tests/test_routes.py
index 4aa826c40..8b316b70a 100644
--- a/solstone/apps/observer/tests/test_routes.py
+++ b/solstone/apps/observer/tests/test_routes.py
@@ -2313,7 +2313,7 @@ def test_ingest_partial_duplicate_creates_new_segment(observer_env):
assert second_data["segment"] != first_segment
-def test_ingest_partial_match_logged_in_history(observer_env):
+def test_ingest_partial_match_history(observer_env):
"""Test that partial SHA256 matches are logged in history record."""
env = observer_env()
diff --git a/solstone/apps/observer/tests/test_share_delete.py b/solstone/apps/observer/tests/test_share_delete.py
index 02c65c888..6421d7cd8 100644
--- a/solstone/apps/observer/tests/test_share_delete.py
+++ b/solstone/apps/observer/tests/test_share_delete.py
@@ -30,6 +30,9 @@ def _set_journal(tmp_path, monkeypatch) -> Path:
journal = tmp_path / "journal"
journal.mkdir()
monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal))
+ import solstone.think.utils as think_utils
+
+ think_utils._journal_path_cache = None
return journal
diff --git a/solstone/apps/search/tests/test_pagination.py b/solstone/apps/search/tests/test_pagination.py
index 909d2dada..1955c9e58 100644
--- a/solstone/apps/search/tests/test_pagination.py
+++ b/solstone/apps/search/tests/test_pagination.py
@@ -20,7 +20,6 @@ def search_client(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/settings/call.py b/solstone/apps/settings/call.py
index c3ba1db8c..8fb2c981f 100644
--- a/solstone/apps/settings/call.py
+++ b/solstone/apps/settings/call.py
@@ -13,7 +13,6 @@ import typer
from solstone.convey.reasons import (
INVALID_CONFIG_VALUE,
- NETWORK_SECURITY_REQUIRES_PASSWORD,
)
from solstone.think.convey_client import ConveyClientError, convey_cli, get_client
@@ -54,8 +53,6 @@ observer_app = typer.Typer(help="Observer capture settings.")
app.add_typer(observer_app, name="observer")
convey_app = typer.Typer(help="Convey access configuration.")
app.add_typer(convey_app, name="convey")
-trust_localhost_app = typer.Typer(help="Localhost password-bypass behavior.")
-convey_app.add_typer(trust_localhost_app, name="trust-localhost")
def _request(
@@ -115,29 +112,6 @@ def _moved_stub(command: str) -> None:
raise typer.Exit(2)
-@trust_localhost_app.command("enable")
-@convey_cli
-def convey_trust_localhost_enable() -> None:
- """Enable localhost password bypass."""
-
- _post_config("convey", {"trust_localhost": True})
- typer.echo("localhost trust enabled. localhost requests skip the password.")
-
-
-@trust_localhost_app.command("disable")
-@convey_cli
-def convey_trust_localhost_disable() -> None:
- """Disable localhost password bypass."""
-
- try:
- _post_config("convey", {"trust_localhost": False})
- except ConveyClientError as err:
- if err.reason_code == NETWORK_SECURITY_REQUIRES_PASSWORD.code:
- _exit_with(err.detail or err.error)
- raise
- typer.echo("localhost trust disabled. localhost requests now require the password.")
-
-
@convey_app.command("host-url")
@convey_cli
def convey_host_url(
diff --git a/solstone/apps/settings/copy.py b/solstone/apps/settings/copy.py
index 956c62888..8f15f633c 100644
--- a/solstone/apps/settings/copy.py
+++ b/solstone/apps/settings/copy.py
@@ -5,7 +5,6 @@
from __future__ import annotations
-CONVEY_REFUSE_NO_PASSWORD_TRUST = "error: disabling localhost trust requires a password (otherwise no client could authenticate). set one first with: journal password set"
LOCAL_ENDPOINT_DISCLOSURE = "inference runs on the endpoint you configured; your requests, including screen content for vision, are sent there."
FACET_DETAIL_SUCCESS_HEADING = "{title} is ready"
FACET_DETAIL_VALUE_FRAMING = (
@@ -17,7 +16,6 @@ FACET_DETAIL_SECONDARY_CTA = "create another facet"
FACET_DETAIL_TERTIARY_ESCAPE = "back to settings"
__all__ = [
- "CONVEY_REFUSE_NO_PASSWORD_TRUST",
"FACET_DETAIL_PRIMARY_CTA",
"FACET_DETAIL_SECONDARY_CTA",
"FACET_DETAIL_SUCCESS_HEADING",
diff --git a/solstone/apps/settings/routes.py b/solstone/apps/settings/routes.py
index ceeecc528..36e45c637 100644
--- a/solstone/apps/settings/routes.py
+++ b/solstone/apps/settings/routes.py
@@ -23,9 +23,6 @@ from solstone.apps.chat.config import (
)
from solstone.apps.settings import copy as settings_copy
from solstone.apps.settings import install_copy, transcribe_resource
-from solstone.apps.settings.copy import (
- CONVEY_REFUSE_NO_PASSWORD_TRUST,
-)
from solstone.apps.utils import log_app_action
from solstone.convey import chat_stream, state
from solstone.convey import copy as convey_copy
@@ -40,7 +37,6 @@ from solstone.convey.reasons import (
INVALID_REQUEST_VALUE,
MISSING_REQUEST_BODY,
MISSING_REQUIRED_FIELD,
- NETWORK_SECURITY_REQUIRES_PASSWORD,
SETTINGS_OPERATION_FAILED,
)
from solstone.convey.sol_initiated import copy as sol_voice_copy
@@ -132,11 +128,6 @@ def _compute_runtime_label() -> str:
return "unsupported"
-def _convey_password_is_set(config: dict[str, Any]) -> bool:
- password_hash = config.get("convey", {}).get("password_hash", "")
- return bool(str(password_hash or "").strip())
-
-
def _service_key_validation(config: dict[str, Any]) -> dict[str, Any]:
providers_config = config.get("providers", {})
key_validation = (
@@ -163,9 +154,8 @@ def _project_public_config(config: dict[str, Any]) -> dict[str, Any]:
projected.pop("providers", None)
convey_config = projected.setdefault("convey", {})
convey_config.pop("secret", None)
- has_pw = bool(convey_config.pop("password_hash", None))
+ convey_config.pop("password_hash", None)
convey_config.pop("password", None)
- convey_config["has_password"] = has_pw
projected["runtime_env"] = {k: bool(os.getenv(k)) for k in API_KEY_ENV_VARS}
return projected
@@ -236,8 +226,7 @@ def update_config() -> Any:
"""Update the journal configuration.
Accepts JSON with a 'section' key and per-section config fields to update.
- Supported writes include identity and transcribe settings, convey security
- settings (password, trust_localhost), and API-key env vars.
+ Supported writes include identity and transcribe settings, and API-key env vars.
"""
try:
request_data = request.get_json()
@@ -274,7 +263,6 @@ def update_config() -> Any:
"timezone",
],
"transcribe": ["backend", "enrich", "preserve_all", "noise_upgrade"],
- "convey": ["password", "trust_localhost"],
"support": ["enabled", "proactive", "anonymous_feedback", "portal_url"],
"agent": ["name", "name_status", "named_date"],
"env": API_KEY_ENV_VARS,
@@ -307,35 +295,6 @@ def update_config() -> Any:
changed_fields = {}
old_section = old_config.get(section, {})
- if section == "convey" and "password" in data:
- raw_password = data.pop("password") or ""
- if raw_password:
- if len(raw_password) < 8:
- return error_response(
- INVALID_CONFIG_VALUE,
- detail="Password must be at least 8 characters",
- )
- from werkzeug.security import generate_password_hash
-
- config["convey"]["password_hash"] = generate_password_hash(raw_password)
- changed_fields["password"] = {
- "old": old_section.get("password_hash"),
- "new": config["convey"]["password_hash"],
- }
-
- has_password = _convey_password_is_set(config)
-
- if (
- section == "convey"
- and "trust_localhost" in data
- and not bool(data["trust_localhost"])
- and not has_password
- ):
- return error_response(
- NETWORK_SECURITY_REQUIRES_PASSWORD,
- detail=CONVEY_REFUSE_NO_PASSWORD_TRUST,
- )
-
# Update only allowed fields
for key in allowed_sections[section]:
if key in data:
@@ -401,10 +360,7 @@ def update_config() -> Any:
# Log if something changed (don't log sensitive values)
if changed_fields:
log_fields = changed_fields
- if section == "convey" and "password" in log_fields:
- # Don't log actual password values
- log_fields = {"password": {"old": "***", "new": "***"}}
- elif section == "env":
+ if section == "env":
# Don't log actual API key values
log_fields = {k: {"old": "***", "new": "***"} for k in changed_fields}
@@ -429,10 +385,6 @@ def update_config() -> Any:
return _settings_operation_failed()
-def _trust_localhost_enabled(config: dict[str, Any]) -> bool:
- return bool(config.get("convey", {}).get("trust_localhost", True))
-
-
def _host_url_status_value() -> str:
from solstone.think.pairing.config import get_host_url
@@ -497,14 +449,11 @@ def convey_status() -> Any:
from solstone.think.service import DEFAULT_SERVICE_PORT
from solstone.think.utils import read_service_port
- config = get_journal_config()
bind_host = _resolve_bind_host()
port = read_service_port("convey") or DEFAULT_SERVICE_PORT
status_text = convey_copy.format_convey_status(
bind=f"{bind_host}:{port}",
host_url=_host_url_status_value(),
- password="set" if _convey_password_is_set(config) else "not set",
- trust_localhost="yes" if _trust_localhost_enabled(config) else "no",
)
return jsonify({"status_text": status_text})
except Exception:
diff --git a/solstone/apps/settings/tests/test_chat_routes.py b/solstone/apps/settings/tests/test_chat_routes.py
index e44265248..01d8aa513 100644
--- a/solstone/apps/settings/tests/test_chat_routes.py
+++ b/solstone/apps/settings/tests/test_chat_routes.py
@@ -13,7 +13,6 @@ from solstone.convey import create_app
def _base_config() -> dict:
return {
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
diff --git a/solstone/apps/settings/tests/test_facet_config_routes.py b/solstone/apps/settings/tests/test_facet_config_routes.py
index 6b5cf3fa1..76e9cb180 100644
--- a/solstone/apps/settings/tests/test_facet_config_routes.py
+++ b/solstone/apps/settings/tests/test_facet_config_routes.py
@@ -15,7 +15,6 @@ def _settings_client(settings_env):
config_path = journal_path / "config" / "journal.json"
config = json.loads(config_path.read_text(encoding="utf-8"))
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
app = create_app(str(journal_path))
app.config["TESTING"] = True
diff --git a/solstone/apps/settings/tests/test_facet_detail.py b/solstone/apps/settings/tests/test_facet_detail.py
index ca4f8cac3..3b5d57410 100644
--- a/solstone/apps/settings/tests/test_facet_detail.py
+++ b/solstone/apps/settings/tests/test_facet_detail.py
@@ -41,7 +41,6 @@ def _settings_client(settings_env):
config_path = journal_path / "config" / "journal.json"
config = json.loads(config_path.read_text(encoding="utf-8"))
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
app = create_app(str(journal_path))
app.config["TESTING"] = True
diff --git a/solstone/apps/settings/tests/test_security_copy.py b/solstone/apps/settings/tests/test_security_copy.py
deleted file mode 100644
index 28f1fb301..000000000
--- a/solstone/apps/settings/tests/test_security_copy.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright (c) 2026 sol pbc
-
-from __future__ import annotations
-
-import re
-
-from solstone.convey import copy as convey_copy
-
-
-def test_security_reach_hint_is_locked():
- assert convey_copy.SETTINGS_SECURITY_REACH_HINT == (
- "how your devices reach home — managing your connection, paired devices, "
- "and reach-from-anywhere now lives in link →"
- )
-
-
-def test_security_reach_hint_stays_in_bounds():
- lowered = convey_copy.SETTINGS_SECURITY_REACH_HINT.lower()
-
- assert "account" not in lowered
- assert not re.search(r"\b(dl|pl|spl)\b", lowered)
- assert "phone" not in lowered
- assert "iphone" not in lowered
-
-
-def test_orphaned_network_mode_constants_removed():
- for name in (
- "SETTINGS_NETWORK_MODE_LABEL",
- "SETTINGS_NETWORK_MODE_OFF",
- "SETTINGS_NETWORK_MODE_ON",
- "SETTINGS_NETWORK_DESC_OFF",
- "SETTINGS_NETWORK_DESC_ON",
- "SETTINGS_NETWORK_BUTTON_ENABLE",
- "SETTINGS_NETWORK_BUTTON_DISABLE",
- "SETTINGS_NETWORK_DISCLOSURE_TITLE",
- "SETTINGS_NETWORK_DISCLOSURE_BODY",
- "SETTINGS_NETWORK_DISCLOSURE_PASSWORD_LABEL",
- "SETTINGS_NETWORK_DISCLOSURE_CONFIRM_LABEL",
- "SETTINGS_NETWORK_DISCLOSURE_SUBMIT",
- "SETTINGS_NETWORK_DISCLOSURE_MISMATCH",
- "SETTINGS_NETWORK_DISCLOSURE_TOO_SHORT",
- "SETTINGS_NETWORK_RESTARTING",
- ):
- assert not hasattr(convey_copy, name), name
- assert name not in convey_copy.__all__, name
diff --git a/solstone/apps/settings/tests/test_sol_voice_routes.py b/solstone/apps/settings/tests/test_sol_voice_routes.py
index 22d83992c..133e27ec9 100644
--- a/solstone/apps/settings/tests/test_sol_voice_routes.py
+++ b/solstone/apps/settings/tests/test_sol_voice_routes.py
@@ -20,7 +20,6 @@ from solstone.convey.sol_initiated.settings import load_settings
def _base_config() -> dict:
return {
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
diff --git a/solstone/apps/settings/tests/test_transcribe_resource_payload.py b/solstone/apps/settings/tests/test_transcribe_resource_payload.py
index 922d3d8b1..bba9da4ab 100644
--- a/solstone/apps/settings/tests/test_transcribe_resource_payload.py
+++ b/solstone/apps/settings/tests/test_transcribe_resource_payload.py
@@ -50,7 +50,6 @@ def _client(journal_path):
def _ready_journal(settings_env):
journal_path, config = settings_env()
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
(journal_path / "config" / "journal.json").write_text(
json.dumps(config, indent=2) + "\n",
encoding="utf-8",
diff --git a/solstone/apps/settings/tests/test_vision_routes.py b/solstone/apps/settings/tests/test_vision_routes.py
index c0c6a496b..767404823 100644
--- a/solstone/apps/settings/tests/test_vision_routes.py
+++ b/solstone/apps/settings/tests/test_vision_routes.py
@@ -10,7 +10,6 @@ from solstone.think.utils import get_config
def _base_config() -> dict:
return {
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
"describe": {"max_extractions": 37},
}
diff --git a/solstone/apps/settings/tests/test_workspace_html.py b/solstone/apps/settings/tests/test_workspace_html.py
index df4ef378e..175a0519d 100644
--- a/solstone/apps/settings/tests/test_workspace_html.py
+++ b/solstone/apps/settings/tests/test_workspace_html.py
@@ -115,9 +115,12 @@ def test_workspace_cogitate_auth_control_removed():
assert "document.getElementById('field-cogitate-auth')" not in text
-def test_workspace_security_network_mode_ui_removed_and_link_hint_present():
+def test_workspace_security_section_removed():
text = _workspace_text()
for removed in (
+ '
@@ -2117,7 +2116,6 @@ button:focus:not(:focus-visible) {
data
-
@@ -2648,38 +2646,6 @@ button:focus:not(:focus-visible) {
-
-
-
sync
@@ -3197,7 +3163,7 @@ setupModalAccessibility('customActivityModal');
setupModalAccessibility('cleanupModal');
// ========== NAVIGATION ==========
-const VALID_SECTIONS = ['guide', 'profile', 'agent', 'apikeys', 'transcription', 'observer', 'vision', 'sol-voice', 'chat', 'security', 'sync', 'storage', 'support', 'facets', 'facet-appearance', 'facet-activities', 'facet-activity'];
+ const VALID_SECTIONS = ['guide', 'profile', 'agent', 'apikeys', 'transcription', 'observer', 'vision', 'sol-voice', 'chat', 'sync', 'storage', 'support', 'facets', 'facet-appearance', 'facet-activities', 'facet-activity'];
function switchSection(sectionId, updateHash = true) {
// Validate section exists
@@ -3587,11 +3553,6 @@ function populateFields(config) {
document.getElementById('field-transcribe-preserve').checked = transcribe.preserve_all || false;
document.getElementById('field-transcribe-noise-upgrade').checked = transcribe.noise_upgrade !== false;
- // Convey
- const convey = config.convey || {};
- document.getElementById('field-trust-localhost').checked = convey.trust_localhost !== false;
- renderConveyHostFields(config);
-
// Support settings
const support = config.support || {};
document.getElementById('field-support-enabled').checked = support.enabled !== false;
@@ -3984,18 +3945,6 @@ function readAutoSaveControlValue(el) {
function applySavedConfigResult(result, runtimeEnv) {
configData = result.config;
configData.runtime_env = runtimeEnv;
- renderConveyHostFields(configData);
-}
-
-function renderConveyHostFields(config) {
- const convey = config?.convey || {};
- const passwordInput = document.getElementById('field-password');
-
- if (passwordInput) {
- passwordInput.placeholder = convey.has_password
- ? 'Password set (enter to change)'
- : 'Enter password to protect web access';
- }
}
async function saveConfigValue(section, key, value) {
@@ -4585,10 +4534,11 @@ document.getElementById('createPersonalBtn').onclick = async () => {
};
// ========== PASSWORD/KEY TOGGLE ==========
-// Handle all password toggle buttons (password field + API key fields)
+// Handle all password toggle buttons for API key fields
document.querySelectorAll('.password-toggle').forEach(btn => {
btn.onclick = () => {
- const targetId = btn.dataset.toggle || 'field-password';
+ const targetId = btn.dataset.toggle;
+ if (!targetId) return;
const input = document.getElementById(targetId);
const icon = btn.querySelector('span');
if (input && icon) {
diff --git a/solstone/apps/speakers/tests/conftest.py b/solstone/apps/speakers/tests/conftest.py
index 525d5025e..101d05aa0 100644
--- a/solstone/apps/speakers/tests/conftest.py
+++ b/solstone/apps/speakers/tests/conftest.py
@@ -51,6 +51,12 @@ def speakers_env(tmp_path, monkeypatch):
self.journal = journal_path
monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal_path))
monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")
+ config_path = journal_path / "config" / "journal.json"
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+ config_path.write_text(
+ json.dumps({"setup": {"completed_at": 1700000000000}}) + "\n",
+ encoding="utf-8",
+ )
import solstone.think.utils as think_utils
think_utils._journal_path_cache = None
diff --git a/solstone/apps/stats/static/dashboard.js b/solstone/apps/stats/static/dashboard.js
index 37326ffe6..056ac2873 100644
--- a/solstone/apps/stats/static/dashboard.js
+++ b/solstone/apps/stats/static/dashboard.js
@@ -976,12 +976,12 @@ const Dashboard = (function() {
return {
load: function(url) {
fetch(url, {
- credentials: 'same-origin' // Include cookies for authentication
+ credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
if (response.status === 401 || response.redirected) {
- // Redirected to login, reload the page
+ // Access changed while viewing the dashboard; reload the page.
window.location.reload();
return;
}
diff --git a/solstone/apps/stats/tests/test_stats_data_route.py b/solstone/apps/stats/tests/test_stats_data_route.py
index bebc9579b..837262aa0 100644
--- a/solstone/apps/stats/tests/test_stats_data_route.py
+++ b/solstone/apps/stats/tests/test_stats_data_route.py
@@ -19,7 +19,6 @@ def stats_client(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/stats/tests/test_workspace_template.py b/solstone/apps/stats/tests/test_workspace_template.py
index 850fc247e..12e1d0de4 100644
--- a/solstone/apps/stats/tests/test_workspace_template.py
+++ b/solstone/apps/stats/tests/test_workspace_template.py
@@ -106,7 +106,6 @@ def stats_env(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
index f778dec0b..5d2806261 100644
--- a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
+++ b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
@@ -37,7 +37,6 @@ def _client(journal_path):
def _settings_config() -> dict:
return {
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
- "convey": {"trust_localhost": True},
"providers": {
"generate": {"provider": "google", "tier": 2, "backup": "anthropic"},
"cogitate": {"provider": "openai", "tier": 2, "backup": "anthropic"},
diff --git a/solstone/apps/thinking/tests/test_local_endpoint_routes.py b/solstone/apps/thinking/tests/test_local_endpoint_routes.py
index 7dc5b6044..2ac88347b 100644
--- a/solstone/apps/thinking/tests/test_local_endpoint_routes.py
+++ b/solstone/apps/thinking/tests/test_local_endpoint_routes.py
@@ -37,7 +37,6 @@ def _write_config(journal_path: Path, config: dict[str, Any]) -> None:
def _ready_settings_env(settings_env) -> tuple[Path, dict[str, Any]]:
journal_path, config = settings_env()
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
_write_config(journal_path, config)
return journal_path, config
diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py
index 1f6c5f4a0..1c232e62d 100644
--- a/solstone/apps/thinking/tests/test_providers_payload_extended.py
+++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py
@@ -42,7 +42,6 @@ def settings_client_with_journal(settings_env):
def _settings_client_with_journal(settings_env):
journal_path, config = settings_env()
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
(journal_path / "config" / "journal.json").write_text(
json.dumps(config, indent=2) + "\n",
encoding="utf-8",
diff --git a/solstone/apps/thinking/tests/test_scout_routes.py b/solstone/apps/thinking/tests/test_scout_routes.py
index 4c2d8effb..83ef0024f 100644
--- a/solstone/apps/thinking/tests/test_scout_routes.py
+++ b/solstone/apps/thinking/tests/test_scout_routes.py
@@ -32,6 +32,7 @@ def _read_config(journal: Path) -> dict:
def _write_config(payload: dict) -> None:
+ payload.setdefault("setup", {"completed_at": 1700000000000})
write_journal_config(payload)
@@ -48,9 +49,6 @@ def thinking_client(journal_copy: Path):
app = create_app(journal=str(journal_copy.resolve()))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/solstone/apps/thinking/tests/test_workspace_html.py b/solstone/apps/thinking/tests/test_workspace_html.py
index 4f2d7525b..ace56200e 100644
--- a/solstone/apps/thinking/tests/test_workspace_html.py
+++ b/solstone/apps/thinking/tests/test_workspace_html.py
@@ -18,7 +18,6 @@ STATIC = Path(__file__).resolve().parents[1] / "static" / "thinking.js"
def test_workspace_renders_each_lane(settings_env):
journal_path, config = settings_env()
config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"}
- config.setdefault("convey", {})["trust_localhost"] = True
(journal_path / "config" / "journal.json").write_text(
json.dumps(config, indent=2) + "\n",
encoding="utf-8",
diff --git a/solstone/apps/timeline/tests/conftest.py b/solstone/apps/timeline/tests/conftest.py
index 4e3ec54ce..d7dc41494 100644
--- a/solstone/apps/timeline/tests/conftest.py
+++ b/solstone/apps/timeline/tests/conftest.py
@@ -54,7 +54,6 @@ def timeline_env(tmp_path, monkeypatch):
(journal / "config" / "journal.json").write_text(
json.dumps(
{
- "convey": {"secret": "test-secret", "trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
diff --git a/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json
index 90a89f590..d497db418 100644
--- a/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json
+++ b/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json
@@ -1 +1 @@
-{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}}
+{"setup":{"completed_at":1700000000000}}
\ No newline at end of file
diff --git a/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json
index 90a89f590..d497db418 100644
--- a/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json
+++ b/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json
@@ -1 +1 @@
-{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}}
+{"setup":{"completed_at":1700000000000}}
\ No newline at end of file
diff --git a/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json
index 90a89f590..d497db418 100644
--- a/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json
+++ b/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json
@@ -1 +1 @@
-{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}}
+{"setup":{"completed_at":1700000000000}}
\ No newline at end of file
diff --git a/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json
index 90a89f590..d497db418 100644
--- a/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json
+++ b/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json
@@ -1 +1 @@
-{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}}
+{"setup":{"completed_at":1700000000000}}
\ No newline at end of file
diff --git a/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json
index 90a89f590..d497db418 100644
--- a/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json
+++ b/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json
@@ -1 +1 @@
-{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}}
+{"setup":{"completed_at":1700000000000}}
\ No newline at end of file
diff --git a/solstone/apps/timeline/tests/test_routes.py b/solstone/apps/timeline/tests/test_routes.py
index cbda839ca..01181d88a 100644
--- a/solstone/apps/timeline/tests/test_routes.py
+++ b/solstone/apps/timeline/tests/test_routes.py
@@ -35,7 +35,6 @@ def empty_timeline_env(tmp_path: Path, monkeypatch):
(journal / "config" / "journal.json").write_text(
json.dumps(
{
- "convey": {"secret": "test-secret", "trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
diff --git a/solstone/apps/tokens/tests/conftest.py b/solstone/apps/tokens/tests/conftest.py
index 6d73bc1c7..e99457207 100644
--- a/solstone/apps/tokens/tests/conftest.py
+++ b/solstone/apps/tokens/tests/conftest.py
@@ -25,7 +25,6 @@ def tokens_env(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/apps/transcripts/tests/test_call.py b/solstone/apps/transcripts/tests/test_call.py
index 9364d7e15..f970a146b 100644
--- a/solstone/apps/transcripts/tests/test_call.py
+++ b/solstone/apps/transcripts/tests/test_call.py
@@ -9,18 +9,19 @@ from typer.testing import CliRunner
from solstone.apps.transcripts.call import app
from solstone.think.convey_client import ConveyClient
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.transcripts.call.get_client", lambda: client)
return CliRunner()
@@ -52,7 +53,7 @@ def _write_segment(
def _route_markdown(journal, day: str, params: dict[str, str]) -> str:
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
return client.request("GET", f"/app/transcripts/api/read/{day}", params=params)[
"markdown"
]
diff --git a/solstone/apps/transcripts/tests/test_serve_file.py b/solstone/apps/transcripts/tests/test_serve_file.py
index c0f2caa47..d4966f156 100644
--- a/solstone/apps/transcripts/tests/test_serve_file.py
+++ b/solstone/apps/transcripts/tests/test_serve_file.py
@@ -30,7 +30,6 @@ def client(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
diff --git a/solstone/convey/__init__.py b/solstone/convey/__init__.py
index 4dc19b293..fab0bff29 100644
--- a/solstone/convey/__init__.py
+++ b/solstone/convey/__init__.py
@@ -31,61 +31,6 @@ def __getattr__(name: str):
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
-def _get_or_create_secret() -> str:
- """Load convey.secret from journal.json, generating one if absent."""
- from solstone.think.utils import ensure_journal_config
-
- config = ensure_journal_config()
- return config["convey"]["secret"]
-
-
-def _migrate_password_hash() -> None:
- """Migrate plaintext convey.password to hashed password_hash."""
- from werkzeug.security import generate_password_hash
-
- from solstone.think.journal_config import write_journal_config
- from solstone.think.utils import get_config
-
- config = get_config()
- convey = config.get("convey", {})
-
- if "password_hash" in convey or "password" not in convey:
- return
-
- plaintext = convey.pop("password")
- if plaintext:
- convey["password_hash"] = generate_password_hash(plaintext)
-
- config["convey"] = convey
- write_journal_config(config)
-
-
-def _migrate_setup_completed() -> None:
- """Infer setup.completed_at and set trust_localhost for existing installs.
-
- Legacy migration: handles journals where password_hash was set via
- 'journal password set' CLI before web onboarding existed. Web onboarding
- now writes all config atomically in init_finalize(), so this path is
- only reached for pre-existing journals.
- """
- from solstone.think.journal_config import write_journal_config
- from solstone.think.utils import get_config
-
- config = get_config()
-
- if not config.get("convey", {}).get("password_hash"):
- return
- if config.get("setup", {}).get("completed_at"):
- return
-
- from solstone.think.utils import now_ms
-
- config.setdefault("setup", {})["completed_at"] = now_ms()
- config.setdefault("convey", {})["trust_localhost"] = True
-
- write_journal_config(config)
-
-
def install_identity_stamper(app: Flask) -> None:
from flask import g, request
@@ -155,16 +100,12 @@ def create_app(journal: str = "") -> Flask:
]
)
- app.secret_key = _get_or_create_secret()
- _migrate_password_hash()
- _migrate_setup_completed()
- app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = timedelta(seconds=300)
app.config.setdefault("SECURE_LISTENER_ENABLED", False)
install_identity_stamper(app)
install_request_id_stamper(app)
- # Register root blueprint (login, logout, /, favicon)
+ # Register root blueprint (/, favicon)
app.register_blueprint(root_bp)
# Register config API blueprint
diff --git a/solstone/convey/cli.py b/solstone/convey/cli.py
index ee500ed59..0caf10646 100644
--- a/solstone/convey/cli.py
+++ b/solstone/convey/cli.py
@@ -19,19 +19,6 @@ from .bridge import start_bridge, stop_bridge
logger = logging.getLogger(__name__)
-def _resolve_config_password_hash() -> str:
- """Return the configured Convey password hash from journal config."""
- from solstone.think.utils import get_config
-
- try:
- config = get_config()
- convey_config = config.get("convey", {})
- return convey_config.get("password_hash", "")
- except Exception:
- # Intended fail-closed-on-unreadable-config: no hash means no password auth.
- return ""
-
-
def _resolve_bind_host() -> str:
"""Return Convey's bind host — always loopback; :5015 is never network-exposed."""
return "127.0.0.1"
@@ -90,13 +77,6 @@ def main() -> None:
journal = get_journal()
app = create_app(journal)
- password = _resolve_config_password_hash()
- if password:
- logger.info("Password authentication enabled")
- else:
- logger.warning(
- "no password configured; required only when localhost trust is disabled."
- )
# Write port to health directory for discovery by other tools
write_service_port("convey", args.port)
diff --git a/solstone/convey/copy.py b/solstone/convey/copy.py
index b13069b19..2db38f409 100644
--- a/solstone/convey/copy.py
+++ b/solstone/convey/copy.py
@@ -70,32 +70,17 @@ CONVEY_REPORT_MAILTO_BODY_PREFIX = (
)
CONVEY_REPORT_MAILTO_TRUNCATION_SUFFIX = "\n\n[full report is in your clipboard]\n"
CONVEY_REPORT_BUTTON_LABEL = "Report this"
-LOGIN_NO_PASSWORD_CONFIGURED = "no password is configured. set one in settings → security, or run 'journal password set' from a terminal on this machine."
-SETTINGS_SECURITY_DESC = "password protection for the convey web interface."
-SETTINGS_SECURITY_REACH_HINT = (
- "how your devices reach home — managing your connection, paired devices, "
- "and reach-from-anywhere now lives in link →"
-)
-SETTINGS_PASSWORD_HINT = "used when localhost trust is off. not used otherwise."
OBSERVER_CALLOSUM_LIVE_LABEL = "live"
def format_convey_status(
*,
bind: str,
- password: str,
- trust_localhost: str,
host_url: str,
) -> str:
- """Return the locked five-line convey status block."""
+ """Return the locked convey status block."""
- return (
- "convey\n"
- f" bind: {bind}\n"
- f" password: {password}\n"
- f" trust localhost: {trust_localhost}\n"
- f" host url: {host_url}"
- )
+ return f"convey\n bind: {bind}\n host url: {host_url}"
__all__ = [
@@ -160,10 +145,6 @@ __all__ = [
"CONVEY_REPORT_SUCCESS_BODY",
"CONVEY_REPORT_SUCCESS_BODY_NO_ID",
"CONVEY_REPORT_TITLE",
- "LOGIN_NO_PASSWORD_CONFIGURED",
"OBSERVER_CALLOSUM_LIVE_LABEL",
- "SETTINGS_PASSWORD_HINT",
- "SETTINGS_SECURITY_DESC",
- "SETTINGS_SECURITY_REACH_HINT",
"format_convey_status",
]
diff --git a/solstone/convey/reasons.py b/solstone/convey/reasons.py
index ce1e0c951..67385ed71 100644
--- a/solstone/convey/reasons.py
+++ b/solstone/convey/reasons.py
@@ -157,11 +157,6 @@ BACKUP_UNAVAILABLE = Reason(
"I couldn't start a backup because solstone's background service isn't running. Start it, then try again.",
503,
)
-NETWORK_SECURITY_REQUIRES_PASSWORD = Reason(
- "network_security_requires_password",
- "I couldn't change localhost trust until a password is set.",
- 400,
-)
LOCAL_REQUEST_ONLY = Reason(
"local_request_only",
"I couldn't register that observer because this endpoint serves local requests only.",
diff --git a/solstone/convey/root.py b/solstone/convey/root.py
index 334ebafa1..749fabe08 100644
--- a/solstone/convey/root.py
+++ b/solstone/convey/root.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-"""Root blueprint: authentication and core routes."""
+"""Root blueprint: access gate and core routes."""
from __future__ import annotations
@@ -24,11 +24,9 @@ from flask import (
render_template,
request,
send_from_directory,
- session,
stream_with_context,
url_for,
)
-from werkzeug.security import check_password_hash, generate_password_hash
from solstone.think.cluster import cluster_segments
from solstone.think.journal_config import write_journal_config
@@ -44,7 +42,6 @@ from .config import (
locked_modify_convey_config,
seed_default_app_navigation,
)
-from .copy import LOGIN_NO_PASSWORD_CONFIGURED
from .reasons import INVALID_CONFIG_VALUE, PL_REVOKED
from .secure_listener import get_authorized_clients
from .utils import error_response, error_response_with_reason
@@ -52,17 +49,6 @@ from .utils import error_response, error_response_with_reason
logger = logging.getLogger(__name__)
-def _get_password_hash() -> str:
- """Get current password hash from config, reloading on each call."""
- try:
- config = get_config()
- convey_config = config.get("convey", {})
- return convey_config.get("password_hash", "")
- except Exception:
- # Intended fail-closed-on-unreadable-config: no hash means no password auth.
- return ""
-
-
def _is_setup_complete() -> bool:
"""Check if initial setup has been completed."""
try:
@@ -73,17 +59,6 @@ def _is_setup_complete() -> bool:
return False
-def _check_basic_auth() -> bool:
- """Check Basic Auth credentials against stored password hash."""
- auth = request.authorization
- if not auth or auth.type != "basic":
- return False
- password_hash = _get_password_hash()
- if not password_hash:
- return False
- return check_password_hash(password_hash, auth.password or "")
-
-
bp = Blueprint(
"root",
__name__,
@@ -93,7 +68,7 @@ bp = Blueprint(
@bp.before_app_request
-def require_login() -> Any:
+def require_access() -> Any:
if request.endpoint is None:
return None
@@ -102,7 +77,6 @@ def require_login() -> Any:
"root.init_validate_provider",
"root.init_observers",
"root.init_finalize",
- "root.login",
"static",
"root.static",
"root.favicon",
@@ -143,35 +117,11 @@ def require_login() -> Any:
detail="paired device revoked",
)
- # Session cookie
- if session.get("logged_in"):
- return None
-
- # Basic Auth (per-request, no session creation)
- if _check_basic_auth():
- return None
-
# Check setup state
- setup_complete = _is_setup_complete()
-
- # Opt-in localhost bypass (requires completed setup + trust_localhost flag)
- if setup_complete:
- config = get_config()
- if config.get("convey", {}).get("trust_localhost", True):
- remote_addr = request.remote_addr
- is_localhost = remote_addr in ("127.0.0.1", "::1", "localhost")
- proxy_headers = (
- request.headers.get("X-Forwarded-For")
- or request.headers.get("X-Real-IP")
- or request.headers.get("X-Forwarded-Host")
- )
- if is_localhost and not proxy_headers:
- return None
-
- # Not authenticated — redirect based on setup state
- if not setup_complete:
+ if not _is_setup_complete():
return redirect(url_for("root.init"))
- return redirect(url_for("root.login"))
+
+ return None
@bp.route("/sse/events", methods=["GET"], endpoint="callosum_sse")
@@ -223,26 +173,6 @@ def callosum_sse() -> Response:
)
-@bp.route("/login", methods=["GET", "POST"])
-def login() -> Any:
- # Re-check password from config on each request
- password_hash = _get_password_hash()
-
- # If no password is configured, show error page
- if not password_hash:
- error = LOGIN_NO_PASSWORD_CONFIGURED
- return render_template("login.html", error=error, no_password=True)
-
- error = None
- if request.method == "POST":
- if check_password_hash(password_hash, request.form.get("password", "")):
- session["logged_in"] = True
- session.permanent = True
- return redirect(url_for("root.index"))
- error = "incorrect password. passwords are case-sensitive. if you've forgotten it, you can reset via journal password set on the command line."
- return render_template("login.html", error=error, no_password=False)
-
-
@bp.route("/init")
def init() -> Any:
if _is_setup_complete():
@@ -316,24 +246,10 @@ def init_observers() -> Any:
def init_finalize() -> Any:
data = request.get_json(silent=True) or {}
- password = data.get("password") or ""
- if password and len(password) < 8:
- return error_response(
- INVALID_CONFIG_VALUE,
- detail="Password must be at least 8 characters",
- )
-
from solstone.think.utils import now_ms
config = get_config()
- convey_config = config.setdefault("convey", {})
- convey_config.pop("allow_network_access", None)
- convey_update = {
- "trust_localhost": True,
- }
- if password:
- convey_update["password_hash"] = generate_password_hash(password)
- convey_config.update(convey_update)
+ config.setdefault("convey", {}).pop("allow_network_access", None)
config.setdefault("identity", {}).update(
{
k: v
@@ -375,17 +291,9 @@ def init_finalize() -> Any:
except Exception:
logger.error("default app navigation seed convey-config PERSIST failed")
- session["logged_in"] = True
- session.permanent = True
return jsonify({"success": True, "redirect": url_for("app:thinking.index")})
-@bp.route("/logout")
-def logout() -> Any:
- session.pop("logged_in", None)
- return redirect(url_for("root.login"))
-
-
@bp.route("/favicon.ico")
def favicon() -> Any:
"""Serve the favicon from the project root."""
diff --git a/solstone/convey/static/tests/api.html b/solstone/convey/static/tests/api.html
index 482f499de..b796b4ea6 100644
--- a/solstone/convey/static/tests/api.html
+++ b/solstone/convey/static/tests/api.html
@@ -78,9 +78,9 @@
), error => error.serverMessage === 'Request failed (HTTP 400)');
await expectApiError('401 no redirect', () => withFetch(
- async () => response({ status: 401, statusText: 'Unauthorized', body: '{"error":"login"}' }),
+ async () => response({ status: 401, statusText: 'Unauthorized', body: '{"error":"unauthorized"}' }),
() => window.apiJson('/auth', { noAuthRedirect: true })
- ), error => error.status === 401 && error.serverMessage === 'login');
+ ), error => error.status === 401 && error.serverMessage === 'unauthorized');
await expectApiError('403 no redirect', () => withFetch(
async () => response({ status: 403, statusText: 'Forbidden', body: '{"message":"disabled"}' }),
diff --git a/solstone/convey/templates/login.html b/solstone/convey/templates/login.html
deleted file mode 100644
index d00716db9..000000000
--- a/solstone/convey/templates/login.html
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
- sign in — solstone
-
-
-
-
-
-
sign in to solstone
- {% if error %}
-
- {% endif %}
- {% if not no_password %}
-
- {% endif %}
-
your data stays on your machine
-
-
-
diff --git a/solstone/convey/tests/conftest.py b/solstone/convey/tests/conftest.py
index 73cef1823..49c76f408 100644
--- a/solstone/convey/tests/conftest.py
+++ b/solstone/convey/tests/conftest.py
@@ -22,7 +22,6 @@ def convey_env(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
@@ -59,7 +58,6 @@ def convey_env_setup_pending(tmp_path, monkeypatch):
config_file.write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {},
},
indent=2,
diff --git a/solstone/convey/tests/test_callosum_sse.py b/solstone/convey/tests/test_callosum_sse.py
index f13c3fb30..7d4abe49f 100644
--- a/solstone/convey/tests/test_callosum_sse.py
+++ b/solstone/convey/tests/test_callosum_sse.py
@@ -71,16 +71,19 @@ def test_callosum_sse_success_headers(convey_env):
resp.close()
-def test_callosum_sse_unauthenticated_redirects_to_login(convey_env):
+def test_callosum_sse_setup_complete_serves_with_proxy_header(convey_env):
env = convey_env()
resp = env.client.get(
"/sse/events",
headers={"X-Forwarded-For": "1.2.3.4"},
+ buffered=False,
)
-
- assert resp.status_code == 302
- assert "/login" in resp.headers["Location"]
+ try:
+ assert resp.status_code == 200
+ assert resp.content_type.startswith("text/event-stream")
+ finally:
+ resp.close()
def test_callosum_sse_round_trip_payload(convey_env):
diff --git a/solstone/convey/tests/test_request_id_header.py b/solstone/convey/tests/test_request_id_header.py
index b07cc43e2..ad2e72737 100644
--- a/solstone/convey/tests/test_request_id_header.py
+++ b/solstone/convey/tests/test_request_id_header.py
@@ -19,7 +19,7 @@ def _assert_request_id_header(response) -> None:
def test_request_id_header_on_success(convey_env) -> None:
env = convey_env()
- response = env.client.get("/login")
+ response = env.client.get("/")
_assert_request_id_header(response)
diff --git a/solstone/talent/journal/references/config.md b/solstone/talent/journal/references/config.md
index 7c303a689..66f27d76b 100644
--- a/solstone/talent/journal/references/config.md
+++ b/solstone/talent/journal/references/config.md
@@ -40,20 +40,7 @@ This configuration helps meeting extraction identify the owner as a participant,
## Convey configuration
-The `convey` block contains settings for the web application:
-
-```json
-{
- "convey": {
- "password_hash": " Security or journal password set>"
- }
-}
-```
-
-Fields:
-- `password_hash` (string) – Hashed password for accessing the convey web application. Set via Settings → Security or `journal password set`.
-
-**UI Preferences:** The separate `config/convey.json` file stores UI/UX personalization (facet/app ordering, selected facet). All fields optional:
+The separate `config/convey.json` file stores UI/UX personalization (facet/app ordering, selected facet). All fields optional:
```json
{
diff --git a/solstone/think/journal_default.json b/solstone/think/journal_default.json
index 7462db9ff..ce8ffb7b7 100644
--- a/solstone/think/journal_default.json
+++ b/solstone/think/journal_default.json
@@ -65,9 +65,6 @@
},
"debug_show_throttled": false
},
- "convey": {
- "trust_localhost": true
- },
"pairing": {
"host_url": null
},
diff --git a/solstone/think/password_cli.py b/solstone/think/password_cli.py
deleted file mode 100644
index 7cfc30eea..000000000
--- a/solstone/think/password_cli.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright (c) 2026 sol pbc
-
-"""CLI for setting the convey web UI password.
-
-Usage:
- journal password set Set the convey password
- journal password reset Reset the convey password (alias for set)
-"""
-
-from __future__ import annotations
-
-import argparse
-import getpass
-import sys
-
-from werkzeug.security import generate_password_hash
-
-from solstone.think.journal_config import write_journal_config
-from solstone.think.utils import get_config, require_solstone, setup_cli
-
-
-def _set_password() -> None:
- """Prompt for a password, hash it, and write to journal config."""
- password = getpass.getpass("Password: ")
- confirm = getpass.getpass("Confirm password: ")
-
- if password != confirm:
- print("Passwords do not match.", file=sys.stderr)
- sys.exit(1)
-
- password_hash = generate_password_hash(password)
-
- config = get_config()
- config.setdefault("convey", {})["password_hash"] = password_hash
- config.get("convey", {}).pop("password", None)
-
- write_journal_config(config)
-
- print("Password set successfully.")
-
-
-def main() -> None:
- parser = argparse.ArgumentParser(description="Manage convey web UI password")
- subparsers = parser.add_subparsers(dest="subcommand")
- subparsers.add_parser("set", help="Set the convey password")
- subparsers.add_parser("reset", help="Reset the convey password")
-
- args = setup_cli(parser)
- require_solstone()
-
- if args.subcommand in ("set", "reset"):
- _set_password()
- else:
- parser.print_help()
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/solstone/think/settings_cli.py b/solstone/think/settings_cli.py
index fa710d41f..edb16c428 100644
--- a/solstone/think/settings_cli.py
+++ b/solstone/think/settings_cli.py
@@ -8,28 +8,17 @@ from __future__ import annotations
import argparse
import json
import sys
-from typing import Any
from solstone.convey.cli import _resolve_bind_host
from solstone.convey.copy import format_convey_status
from solstone.think.pairing.config import get_host_url
from solstone.think.service import DEFAULT_SERVICE_PORT
from solstone.think.utils import (
- get_config,
read_service_port,
setup_cli,
)
-def _trust_localhost_enabled(config: dict[str, Any]) -> bool:
- return bool(config.get("convey", {}).get("trust_localhost", True))
-
-
-def _convey_password_is_set(config: dict[str, Any]) -> bool:
- password_hash = config.get("convey", {}).get("password_hash", "")
- return bool(str(password_hash or "").strip())
-
-
def _host_url_status_value() -> str:
return get_host_url()
@@ -38,17 +27,15 @@ def _convey_port() -> int:
return read_service_port("convey") or DEFAULT_SERVICE_PORT
-def _status_payload(config: dict[str, Any]) -> dict[str, Any]:
+def _status_payload() -> dict[str, str]:
return {
"effective_host_url": get_host_url(),
- "password_configured": _convey_password_is_set(config),
}
def _print_status(*, as_json: bool) -> None:
- config = get_config()
if as_json:
- print(json.dumps(_status_payload(config), indent=2))
+ print(json.dumps(_status_payload(), indent=2))
return
bind_host = _resolve_bind_host()
@@ -57,8 +44,6 @@ def _print_status(*, as_json: bool) -> None:
format_convey_status(
bind=f"{bind_host}:{port}",
host_url=_host_url_status_value(),
- password="set" if _convey_password_is_set(config) else "not set",
- trust_localhost="yes" if _trust_localhost_enabled(config) else "no",
)
)
diff --git a/solstone/think/sol_cli.py b/solstone/think/sol_cli.py
index fe9142496..ec074ade0 100644
--- a/solstone/think/sol_cli.py
+++ b/solstone/think/sol_cli.py
@@ -99,7 +99,6 @@ COMMANDS: dict[str, Command] = {
"install-models": Command("solstone.think.install_models", "service"),
"install-provider": Command("solstone.think.install_provider", "service"),
"skills": Command("solstone.think.skills_cli", "access"),
- "password": Command("solstone.think.password_cli", "service"),
"settings": Command("solstone.think.settings_cli", "service"),
"streams": Command("solstone.think.streams", "service"),
"segment": Command("solstone.think.segment", "service"),
diff --git a/solstone/think/utils.py b/solstone/think/utils.py
index 0736213d5..17ba32589 100644
--- a/solstone/think/utils.py
+++ b/solstone/think/utils.py
@@ -17,7 +17,6 @@ import logging
import os
import pwd
import re
-import secrets
import socket
import sys
import time
@@ -730,9 +729,8 @@ def _resolve_os_timezone() -> str:
def ensure_journal_config() -> dict[str, Any]:
"""Materialize /config/journal.json and return its contents.
- Idempotent after first creation, with one transitional exception: if the
- file exists but lacks ``convey.secret``, the secret is backfilled. Identity
- fields on an existing file are never modified.
+ Idempotent after first creation. Identity fields on an existing file are
+ never modified.
"""
from solstone.think.journal_config import write_journal_config
@@ -744,9 +742,6 @@ def ensure_journal_config() -> dict[str, Any]:
if config_path.exists():
with config_path.open(encoding="utf-8") as fh:
config = json.load(fh)
- if not config.get("convey", {}).get("secret"):
- config.setdefault("convey", {})["secret"] = secrets.token_hex(32)
- write_journal_config(config)
return config
if _default_config is None:
@@ -771,7 +766,6 @@ def ensure_journal_config() -> dict[str, Any]:
config["identity"]["name"] = full_name
config["identity"]["preferred"] = login_name
config["identity"]["timezone"] = timezone
- config.setdefault("convey", {})["secret"] = secrets.token_hex(32)
write_journal_config(config)
return config
diff --git a/tests/_baseline_harness.py b/tests/_baseline_harness.py
index de8605658..d939f3dea 100644
--- a/tests/_baseline_harness.py
+++ b/tests/_baseline_harness.py
@@ -14,6 +14,7 @@ oracle.
from __future__ import annotations
import atexit
+import json
import os
import shutil
import subprocess
@@ -136,14 +137,24 @@ def isolated_app_env(journal: Path) -> Iterator[Path]:
os.environ["SOLSTONE_JOURNAL"] = prev_override
-def make_logged_in_test_client(journal: Path):
- """Create a Flask test client with an authenticated session."""
+def make_test_client(journal: Path):
+ """Create a Flask test client for an isolated journal."""
from solstone.convey import create_app
app = create_app(journal=str(Path(journal).resolve()))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
+
+
+def mark_setup_complete(journal: Path, completed_at: int = 1700000000000) -> None:
+ """Mark a minimal test journal as past first-run setup."""
+
+ config_path = Path(journal) / "config" / "journal.json"
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+ if config_path.exists():
+ config = json.loads(config_path.read_text(encoding="utf-8"))
+ else:
+ config = {}
+ config["setup"] = {"completed_at": completed_at}
+ config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
diff --git a/tests/baselines/api/settings/config.json b/tests/baselines/api/settings/config.json
index 0a41ae0f9..7cef39ee8 100644
--- a/tests/baselines/api/settings/config.json
+++ b/tests/baselines/api/settings/config.json
@@ -6,7 +6,6 @@
},
"convey": {
"allow_network_access": false,
- "has_password": true,
"trust_localhost": true
},
"describe": {
diff --git a/tests/conftest.py b/tests/conftest.py
index 093146638..c3fc17ce7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -158,11 +158,16 @@ def set_test_journal_path(monkeypatch, _isolate_os_environ):
This ensures all tests have a valid SOLSTONE_JOURNAL without needing
to explicitly set it in each test.
"""
+ import solstone.think.utils as think_utils
+
monkeypatch.setenv(
"SOLSTONE_JOURNAL",
str(Path("tests/fixtures/journal").resolve()),
)
monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")
+ think_utils._journal_path_cache = None
+ yield
+ think_utils._journal_path_cache = None
@pytest.fixture(autouse=True)
@@ -216,6 +221,9 @@ def journal_copy(tmp_path, monkeypatch):
dst = tmp_path / "journal"
copytree_tracked(src, dst)
monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst.resolve()))
+ import solstone.think.utils as think_utils
+
+ think_utils._journal_path_cache = None
return dst
diff --git a/tests/link/certless_helpers.py b/tests/link/certless_helpers.py
index b4cc29d4a..2e708079a 100644
--- a/tests/link/certless_helpers.py
+++ b/tests/link/certless_helpers.py
@@ -75,10 +75,8 @@ def write_config(
journal: Path,
*,
link: dict[str, Any] | None = None,
- trust_localhost: bool = True,
) -> None:
config: dict[str, Any] = {
- "convey": {"trust_localhost": trust_localhost},
"setup": {"completed_at": 1700000000000},
}
if link is not None:
diff --git a/tests/link/test_link_join_refusals.py b/tests/link/test_link_join_refusals.py
index 87750e09c..bd92001ae 100644
--- a/tests/link/test_link_join_refusals.py
+++ b/tests/link/test_link_join_refusals.py
@@ -59,7 +59,7 @@ def _args() -> argparse.Namespace:
)
-def test_pair_request_refuses_auth_bounce_html(
+def test_pair_request_refuses_html_bounce(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
@@ -68,8 +68,8 @@ def test_pair_request_refuses_auth_bounce_html(
def fake_urlopen(request, **_kwargs):
return _FakeResponse(
- b"login",
- url="http://receiver/login",
+ b"unexpected",
+ url="http://receiver/unexpected",
content_type="text/html",
)
@@ -80,7 +80,7 @@ def test_pair_request_refuses_auth_bounce_html(
assert result == 1
err = capsys.readouterr().err
assert "http://receiver/app/link/by-code" in err
- assert "http://receiver/login" in err
+ assert "http://receiver/unexpected" in err
assert "text/html" in err
diff --git a/tests/link/test_local_endpoints_route.py b/tests/link/test_local_endpoints_route.py
index 06911eb29..5efd8a072 100644
--- a/tests/link/test_local_endpoints_route.py
+++ b/tests/link/test_local_endpoints_route.py
@@ -28,7 +28,6 @@ def link_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1},
}
),
@@ -80,8 +79,6 @@ def test_local_endpoints_returns_watcher_snapshot(link_client) -> None:
def test_local_endpoints_non_loopback_404(link_client) -> None:
- with link_client.session_transaction() as session:
- session["logged_in"] = True
response = link_client.get(
"/app/link/local-endpoints",
environ_base={"REMOTE_ADDR": "192.168.1.5"},
diff --git a/tests/test_activities_call_parity.py b/tests/test_activities_call_parity.py
index 49bafa41a..50488fe48 100644
--- a/tests/test_activities_call_parity.py
+++ b/tests/test_activities_call_parity.py
@@ -14,7 +14,7 @@ from solstone.apps.activities.call import app
from solstone.convey.reasons import ACTIVITIES_BUSY
from solstone.think.convey_client import ConveyClient
from solstone.think.journal_io import LockTimeout
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
DAY = "20260418"
PREV_DAY = "20260417"
@@ -36,7 +36,7 @@ def journal(tmp_path, monkeypatch):
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.activities.call.get_client", lambda: client)
return CliRunner()
@@ -52,7 +52,6 @@ def _seed_journal(
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
diff --git a/tests/test_activities_cli_create.py b/tests/test_activities_cli_create.py
index 51aef743c..2ab664c96 100644
--- a/tests/test_activities_cli_create.py
+++ b/tests/test_activities_cli_create.py
@@ -7,7 +7,7 @@ from typer.testing import CliRunner
from solstone.apps.activities.call import app
from solstone.think.convey_client import ConveyClient
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
runner = CliRunner()
@@ -29,7 +29,6 @@ def _configure_cli_env(tmp_path, monkeypatch) -> None:
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
@@ -51,7 +50,7 @@ def _configure_cli_env(tmp_path, monkeypatch) -> None:
import solstone.think.utils as think_utils
think_utils._journal_path_cache = None
- client = ConveyClient(session=make_logged_in_test_client(tmp_path), base_url="")
+ client = ConveyClient(session=make_test_client(tmp_path), base_url="")
monkeypatch.setattr("solstone.apps.activities.call.get_client", lambda: client)
diff --git a/tests/test_api_baselines.py b/tests/test_api_baselines.py
index ef46aa2f6..a04803f39 100644
--- a/tests/test_api_baselines.py
+++ b/tests/test_api_baselines.py
@@ -14,7 +14,7 @@ from tests._baseline_harness import (
FROZEN_DATE,
FROZEN_TZ_OFFSET,
isolated_app_env,
- make_logged_in_test_client,
+ make_test_client,
prepare_isolated_journal,
)
from tests.conftest import _install_heavy_module_stubs
@@ -64,7 +64,7 @@ def _baseline_journal(tmp_path_factory):
@pytest.fixture(scope="module")
def client(_baseline_journal):
with isolated_app_env(_baseline_journal):
- yield make_logged_in_test_client(_baseline_journal)
+ yield make_test_client(_baseline_journal)
@pytest.fixture(scope="module")
diff --git a/tests/test_app_import_journal.py b/tests/test_app_import_journal.py
index 0fa412b18..3f8b5e700 100644
--- a/tests/test_app_import_journal.py
+++ b/tests/test_app_import_journal.py
@@ -19,7 +19,6 @@ def _temp_journal(monkeypatch, tmp_path):
json.dumps(
{
"setup": {"completed_at": "2026-04-26T00:00:00Z"},
- "convey": {"trust_localhost": True},
}
),
encoding="utf-8",
diff --git a/tests/test_app_news.py b/tests/test_app_news.py
index cc46aea00..072917976 100644
--- a/tests/test_app_news.py
+++ b/tests/test_app_news.py
@@ -20,9 +20,6 @@ def _make_client(journal: Path):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/tests/test_app_reflections.py b/tests/test_app_reflections.py
index 8c4bc4745..bc62c35f0 100644
--- a/tests/test_app_reflections.py
+++ b/tests/test_app_reflections.py
@@ -31,9 +31,6 @@ def _make_client(journal: Path):
app = create_app(str(journal))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/tests/test_app_support.py b/tests/test_app_support.py
index 550c23790..806fbab8b 100644
--- a/tests/test_app_support.py
+++ b/tests/test_app_support.py
@@ -49,10 +49,11 @@ def journal(tmp_path, monkeypatch):
@pytest.fixture
def cli(journal, monkeypatch):
from solstone.think.convey_client import ConveyClient
- from tests._baseline_harness import make_logged_in_test_client
+ from tests._baseline_harness import make_test_client, mark_setup_complete
+ mark_setup_complete(journal)
client = ConveyClient(
- session=make_logged_in_test_client(journal),
+ session=make_test_client(journal),
base_url="",
require_service=False,
)
diff --git a/tests/test_awareness_call_parity.py b/tests/test_awareness_call_parity.py
index 25596b6a3..149885299 100644
--- a/tests/test_awareness_call_parity.py
+++ b/tests/test_awareness_call_parity.py
@@ -13,7 +13,7 @@ from solstone.apps.awareness.call import app
from solstone.convey.reasons import AWARENESS_BUSY
from solstone.think.convey_client import ConveyClient
from solstone.think.journal_io import LockTimeout
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
FROZEN_MS = 1700000000000
FROZEN_ISO = "20260415T12:00:00"
@@ -25,12 +25,13 @@ def journal(tmp_path, monkeypatch):
# Env must point at the tmp journal so BOTH the seed helpers (append_log/
# update_state) and the in-process route handlers resolve get_journal() to it.
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.awareness.call.get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_awareness_routes.py b/tests/test_awareness_routes.py
index 838585a67..580d7a2c5 100644
--- a/tests/test_awareness_routes.py
+++ b/tests/test_awareness_routes.py
@@ -3,9 +3,11 @@
from __future__ import annotations
+import json
+
from solstone.apps import AppRegistry
from solstone.think.awareness import append_log
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
PREFIX = "/app/awareness"
@@ -28,7 +30,7 @@ def test_awareness_api_only_discovery_registers_blueprint_outside_menu():
def test_awareness_index_404(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/")
@@ -37,7 +39,13 @@ def test_awareness_index_404(journal_copy):
def test_awareness_state_empty_journal_returns_empty_dict(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- client = make_logged_in_test_client(tmp_path)
+ config_dir = tmp_path / "config"
+ config_dir.mkdir(parents=True, exist_ok=True)
+ (config_dir / "journal.json").write_text(
+ json.dumps({"setup": {"completed_at": 1}}),
+ encoding="utf-8",
+ )
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/api/state")
@@ -46,7 +54,7 @@ def test_awareness_state_empty_journal_returns_empty_dict(tmp_path, monkeypatch)
def test_awareness_state_full_state_includes_journal(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/state")
@@ -55,7 +63,7 @@ def test_awareness_state_full_state_includes_journal(journal_copy):
def test_awareness_state_known_section(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/state?section=journal")
@@ -64,7 +72,7 @@ def test_awareness_state_known_section(journal_copy):
def test_awareness_state_unknown_section(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/state?section=nope")
@@ -73,7 +81,7 @@ def test_awareness_state_unknown_section(journal_copy):
def test_awareness_imports_get_defaults(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/imports")
@@ -82,7 +90,7 @@ def test_awareness_imports_get_defaults(journal_copy):
def test_awareness_imports_post_record(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/imports", json={"record": "chatgpt"})
@@ -93,7 +101,7 @@ def test_awareness_imports_post_record(journal_copy):
def test_awareness_imports_post_declined(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/imports", json={"declined": True})
@@ -102,7 +110,7 @@ def test_awareness_imports_post_declined(journal_copy):
def test_awareness_imports_post_nudge(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/imports", json={"nudge": True})
@@ -111,7 +119,7 @@ def test_awareness_imports_post_nudge(journal_copy):
def test_awareness_imports_post_multi_action_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(
f"{PREFIX}/api/imports",
@@ -122,7 +130,7 @@ def test_awareness_imports_post_multi_action_400(journal_copy):
def test_awareness_imports_post_zero_action_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/imports", json={})
@@ -133,7 +141,7 @@ def test_awareness_log_collection_limit_and_kind_filter(journal_copy):
append_log("observation", message="a")
append_log("observation", message="b")
append_log("nudge", message="c")
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/log?limit=2")
@@ -152,7 +160,7 @@ def test_awareness_log_collection_limit_and_kind_filter(journal_copy):
def test_awareness_log_day_param_uses_requested_day(journal_copy):
append_log("observation", message="old", day="20260101")
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.get(f"{PREFIX}/api/log?day=20260101")
@@ -168,7 +176,7 @@ def test_awareness_log_day_param_uses_requested_day(journal_copy):
def test_awareness_log_post_creates_201(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(
f"{PREFIX}/api/log",
@@ -182,7 +190,7 @@ def test_awareness_log_post_creates_201(journal_copy):
def test_awareness_log_post_missing_kind_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/log", json={})
@@ -190,7 +198,7 @@ def test_awareness_log_post_missing_kind_400(journal_copy):
def test_awareness_log_post_empty_kind_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
response = client.post(f"{PREFIX}/api/log", json={"kind": ""})
@@ -198,7 +206,7 @@ def test_awareness_log_post_empty_kind_400(journal_copy):
def test_awareness_post_endpoints_no_body_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
imports_response = client.post(f"{PREFIX}/api/imports")
log_response = client.post(f"{PREFIX}/api/log")
@@ -210,7 +218,7 @@ def test_awareness_post_endpoints_no_body_400(journal_copy):
def test_awareness_post_endpoints_non_json_400(journal_copy):
- client = make_logged_in_test_client(journal_copy)
+ client = make_test_client(journal_copy)
imports_response = client.post(
f"{PREFIX}/api/imports",
diff --git a/tests/test_config_ingest.py b/tests/test_config_ingest.py
index a139ad45d..f1443d12e 100644
--- a/tests/test_config_ingest.py
+++ b/tests/test_config_ingest.py
@@ -94,7 +94,6 @@ def _sample_config():
"allow_network_access": False,
"password_hash": "secret_hash",
"secret": "secret_value",
- "trust_localhost": True,
},
"setup": {"completed_at": 12345},
"providers": {"auth": "provider_auth_val", "key_validation": "val123"},
@@ -188,7 +187,7 @@ def test_config_staged(ingest_env):
source = load_journal_source(env["key"])
assert response.status_code == 200
- assert body == {"staged": True, "skipped": False, "diff_fields": 12}
+ assert body == {"staged": True, "skipped": False, "diff_fields": 11}
assert (state_dir / "source_config.json").exists()
assert (state_dir / "diff.json").exists()
assert "last_hash" in _read_json(state_dir / "state.json")
diff --git a/tests/test_ensure_journal_config.py b/tests/test_ensure_journal_config.py
index 084ef8a06..e70683fef 100644
--- a/tests/test_ensure_journal_config.py
+++ b/tests/test_ensure_journal_config.py
@@ -36,7 +36,7 @@ def test_ensure_journal_config_creates_file_with_os_defaults(tmp_path, monkeypat
assert config["identity"]["name"] == "Test User"
assert config["identity"]["preferred"] == "tester"
assert config["identity"]["timezone"] == "America/Denver"
- assert config["convey"]["secret"]
+ assert "convey" not in config
def test_ensure_journal_config_is_idempotent(tmp_path, monkeypatch):
@@ -98,9 +98,9 @@ def test_ensure_journal_config_timezone_resolver_failure_is_isolated(
assert _config_path(tmp_path).exists()
-def test_ensure_journal_config_backfills_secret_without_touching_identity(
+def test_ensure_journal_config_reads_existing_config_without_touching_identity(
tmp_path, monkeypatch
-):
+) -> None:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
_mock_os(monkeypatch, identity=("OS User", "osuser"), timezone="America/New_York")
config_path = _config_path(tmp_path)
@@ -111,15 +111,12 @@ def test_ensure_journal_config_backfills_secret_without_touching_identity(
"preferred": "Existing",
"timezone": "UTC",
},
- "convey": {"trust_localhost": True},
}
config_path.write_text(json.dumps(staged), encoding="utf-8")
config = utils.ensure_journal_config()
- assert config["identity"] == staged["identity"]
- assert config["convey"]["trust_localhost"] is True
- assert config["convey"]["secret"]
+ assert config == staged
def test_ensure_journal_config_raises_on_corrupt_existing_config_without_writing(
diff --git a/tests/test_entities_call_parity.py b/tests/test_entities_call_parity.py
index 41a1a3e3e..b34122a48 100644
--- a/tests/test_entities_call_parity.py
+++ b/tests/test_entities_call_parity.py
@@ -27,7 +27,7 @@ from solstone.think.entities.journal import load_journal_entity, save_journal_en
from solstone.think.entities.observations import add_observation, save_observations
from solstone.think.entities.review_candidates import save_candidates
from solstone.think.journal_io import LockTimeout
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
runner = CliRunner()
@@ -37,7 +37,7 @@ def _entities_client(monkeypatch: pytest.MonkeyPatch) -> None:
def client() -> ConveyClient:
journal = Path(os.environ["SOLSTONE_JOURNAL"])
return ConveyClient(
- session=make_logged_in_test_client(journal),
+ session=make_test_client(journal),
base_url="",
)
@@ -123,7 +123,6 @@ def _ensure_config(journal: Path) -> None:
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
}
)
diff --git a/tests/test_export.py b/tests/test_export.py
index 948c09a05..ab45e6d3b 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -215,7 +215,6 @@ def _setup_config(tmp_path):
"allow_network_access": False,
"password_hash": "secret_hash",
"secret": "secret_val",
- "trust_localhost": True,
},
"setup": {"completed_at": 12345},
"env": {"KEY": "val"},
@@ -1182,9 +1181,8 @@ class TestExportConfig:
"json"
) or mock_session.post.call_args[1].get("json")
posted_config = posted_data["config"]
- assert posted_config["convey"] == {
- "allow_network_access": False,
- "trust_localhost": True,
- }
+ assert posted_config["convey"] == {"allow_network_access": False}
+ assert "password_hash" not in posted_config["convey"]
+ assert "secret" not in posted_config["convey"]
assert posted_config["setup"] == {"completed_at": 12345}
assert posted_config["env"] == {"KEY": "val"}
diff --git a/tests/test_export_integration.py b/tests/test_export_integration.py
index 6cd3ab129..1bf2a1c65 100644
--- a/tests/test_export_integration.py
+++ b/tests/test_export_integration.py
@@ -278,7 +278,6 @@ def _setup_config(journal_root: Path) -> None:
"retention": {"days": 30},
"convey": {
"allow_network_access": False,
- "trust_localhost": True,
"secret": "shhh",
},
"env": {"API_KEY": "xyz"},
diff --git a/tests/test_export_pl.py b/tests/test_export_pl.py
index a30b25349..95c243e43 100644
--- a/tests/test_export_pl.py
+++ b/tests/test_export_pl.py
@@ -301,7 +301,6 @@ def test_export_dl_regression_config_url_headers_and_body(
"convey": {
"password_hash": "secret",
"secret": "secret",
- "trust_localhost": True,
},
}
(config_dir / "journal.json").write_text(json.dumps(config), encoding="utf-8")
@@ -329,7 +328,7 @@ def test_export_dl_regression_config_url_headers_and_body(
)
payload = mock_session.post.call_args.kwargs["json"]["config"]
assert payload["identity"] == {"name": "Test"}
- assert payload["convey"] == {"trust_localhost": True}
+ assert payload["convey"] == {}
class _FakeInput(io.StringIO):
diff --git a/tests/test_health_call_parity.py b/tests/test_health_call_parity.py
index d04b8d2f7..e60eaf4bd 100644
--- a/tests/test_health_call_parity.py
+++ b/tests/test_health_call_parity.py
@@ -13,7 +13,7 @@ from typer.testing import CliRunner
from solstone.think.convey_client import ConveyClient
from solstone.think.surfaces import health as health_surface
from solstone.think.tools.health import app
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
from tests.test_surfaces_health import (
_clear_readiness_snapshot,
_minimal_facet_tree,
@@ -25,12 +25,13 @@ from tests.test_surfaces_health import (
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.think.tools.health.get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_health_routes.py b/tests/test_health_routes.py
index 0284f694a..e1cbd72b9 100644
--- a/tests/test_health_routes.py
+++ b/tests/test_health_routes.py
@@ -3,9 +3,11 @@
from __future__ import annotations
+import json
+
from solstone.convey import create_app
from solstone.think.surfaces import health as health_surface
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
from tests.test_surfaces_health import (
_clear_readiness_snapshot,
_minimal_facet_tree,
@@ -27,6 +29,16 @@ def _assert_error(response, status: int) -> dict:
def _configure_journal(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ config_dir = tmp_path / "config"
+ config_dir.mkdir(parents=True, exist_ok=True)
+ (config_dir / "journal.json").write_text(
+ json.dumps({"setup": {"completed_at": 1}}),
+ encoding="utf-8",
+ )
+
+
+def _configure_unset_journal(tmp_path, monkeypatch) -> None:
+ monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
def _freeze_health_surface(tmp_path, monkeypatch) -> None:
@@ -47,7 +59,7 @@ def _freeze_health_surface(tmp_path, monkeypatch) -> None:
def test_summary_returns_report_shape(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_freeze_health_surface(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/summary?day=20260410")
@@ -64,7 +76,7 @@ def test_summary_returns_report_shape(tmp_path, monkeypatch):
def test_summary_and_full_identical_for_same_day(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_freeze_health_surface(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
summary_response = client.get(f"{PREFIX}/summary?day=20260410")
full_response = client.get(f"{PREFIX}/full?day=20260410")
@@ -77,7 +89,7 @@ def test_summary_and_full_identical_for_same_day(tmp_path, monkeypatch):
def test_none_field_survives_as_json_null(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_freeze_health_surface(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/summary?day=20260410")
@@ -87,7 +99,7 @@ def test_none_field_survives_as_json_null(tmp_path, monkeypatch):
def test_malformed_day_returns_400(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}/summary?day=notaday"), 400)
@@ -97,7 +109,7 @@ def test_malformed_day_returns_400(tmp_path, monkeypatch):
def test_range_valid_window(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_freeze_health_surface(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/range?day_from=20260404&day_to=20260410")
@@ -108,7 +120,7 @@ def test_range_valid_window(tmp_path, monkeypatch):
def test_range_omit_both_uses_default_window(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_freeze_health_surface(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/range")
@@ -118,7 +130,7 @@ def test_range_omit_both_uses_default_window(tmp_path, monkeypatch):
def test_range_only_one_endpoint_returns_400(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}/range?day_from=20260404"), 400)
@@ -128,7 +140,7 @@ def test_range_only_one_endpoint_returns_400(tmp_path, monkeypatch):
def test_range_inverted_returns_400_distinct_detail(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(
client.get(f"{PREFIX}/range?day_from=20260410&day_to=20260404"),
@@ -139,8 +151,8 @@ def test_range_inverted_returns_400_distinct_detail(tmp_path, monkeypatch):
assert "day_from must be <= day_to" in data["detail"]
-def test_health_requires_login(tmp_path, monkeypatch):
- _configure_journal(tmp_path, monkeypatch)
+def test_health_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch):
+ _configure_unset_journal(tmp_path, monkeypatch)
app = create_app(journal=str(tmp_path))
app.config["TESTING"] = True
client = app.test_client()
@@ -148,3 +160,4 @@ def test_health_requires_login(tmp_path, monkeypatch):
response = client.get(f"{PREFIX}/summary")
assert response.status_code == 302
+ assert "/init" in response.headers["Location"]
diff --git a/tests/test_home_reflections.py b/tests/test_home_reflections.py
index ab94a59f3..90ec6d0ac 100644
--- a/tests/test_home_reflections.py
+++ b/tests/test_home_reflections.py
@@ -12,9 +12,6 @@ def _make_client(journal_path: str):
app = create_app(journal_path)
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
- session.permanent = True
return client
diff --git a/tests/test_import_call.py b/tests/test_import_call.py
index e01beebf3..6b7031671 100644
--- a/tests/test_import_call.py
+++ b/tests/test_import_call.py
@@ -20,7 +20,7 @@ from solstone.think.entities.journal import (
save_journal_entity,
)
from solstone.think.entities.relationships import load_facet_relationship
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
import_call = import_module("solstone.apps.import.call")
import_resolve = import_module("solstone.apps.import.resolve")
@@ -70,6 +70,7 @@ def import_env(tmp_path, monkeypatch):
parents=True, exist_ok=True
)
(tmp_path / "config").mkdir(parents=True, exist_ok=True)
+ mark_setup_complete(tmp_path)
key = generate_key()
source = _source(key=key)
@@ -81,7 +82,7 @@ def import_env(tmp_path, monkeypatch):
def _client() -> ConveyClient:
journal = Path(os.environ["SOLSTONE_JOURNAL"])
return ConveyClient(
- session=make_logged_in_test_client(journal),
+ session=make_test_client(journal),
base_url="",
)
@@ -483,7 +484,10 @@ def test_resolve_config_apply(import_env):
)
_write_json(
import_env["root"] / "config" / "journal.json",
- {"identity": {"name": "Local User"}},
+ {
+ "identity": {"name": "Local User"},
+ "setup": {"completed_at": 1700000000000},
+ },
)
result = runner.invoke(
@@ -527,7 +531,11 @@ def test_resolve_config_keep(import_env):
{"retention": {"days": 30}},
)
_write_json(
- import_env["root"] / "config" / "journal.json", {"retention": {"days": 90}}
+ import_env["root"] / "config" / "journal.json",
+ {
+ "retention": {"days": 90},
+ "setup": {"completed_at": 1700000000000},
+ },
)
result = runner.invoke(
@@ -573,7 +581,11 @@ def test_resolve_config_all_transferable(import_env):
)
_write_json(
import_env["root"] / "config" / "journal.json",
- {"identity": {"name": "Local User"}, "retention": {"days": 90}},
+ {
+ "identity": {"name": "Local User"},
+ "retention": {"days": 90},
+ "setup": {"completed_at": 1700000000000},
+ },
)
result = runner.invoke(
diff --git a/tests/test_init.py b/tests/test_init.py
index 55ecf9904..c2dab18de 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-import base64
import json
import re
from pathlib import Path
@@ -32,11 +31,8 @@ def _make_empty_client(tmp_path, monkeypatch, *, timezone="America/Denver"):
return app.test_client(), journal
-def _remove_password(journal_dir):
+def _clear_setup(journal_dir):
config = _read_config(journal_dir)
- config["convey"].pop("password_hash", None)
- config["convey"].pop("password", None)
- config["convey"].pop("trust_localhost", None)
config.pop("setup", None)
(journal_dir / "config" / "journal.json").write_text(json.dumps(config, indent=2))
@@ -68,7 +64,7 @@ def _save_test_observer(
@pytest.fixture
def fresh_client(journal_copy):
- _remove_password(journal_copy)
+ _clear_setup(journal_copy)
app = create_app(str(journal_copy))
app.config["TESTING"] = True
return app.test_client()
@@ -82,15 +78,15 @@ def configured_client(journal_copy):
class TestInitDetection:
- def test_redirects_to_init_when_no_password(self, fresh_client):
- resp = fresh_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"})
+ def test_redirects_to_init_when_setup_incomplete(self, fresh_client):
+ resp = fresh_client.get("/")
assert resp.status_code == 302
assert "/init" in resp.headers["Location"]
- def test_redirects_to_login_when_password_exists(self, configured_client):
- resp = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"})
+ def test_setup_complete_serves_root(self, configured_client):
+ resp = configured_client.get("/")
assert resp.status_code == 302
- assert "/login" in resp.headers["Location"]
+ assert resp.headers["Location"].endswith("/app/home/")
def test_init_page_renders(self, fresh_client):
resp = fresh_client.get("/init")
@@ -251,7 +247,6 @@ class TestInitDetection:
def test_init_retention_reflects_persisted_state(self, journal_copy):
config = _read_config(journal_copy)
config.pop("setup", None)
- config["convey"].pop("password_hash", None)
config["retention"] = {"raw_media": "days", "raw_media_days": 14}
(journal_copy / "config" / "journal.json").write_text(
json.dumps(config, indent=2)
@@ -302,14 +297,13 @@ class TestInitDetection:
assert config["identity"]["name"] == "OS User"
assert config["identity"]["preferred"] == "osuser"
assert config["identity"]["timezone"] == "America/Denver"
- assert config["convey"]["secret"]
+ assert "convey" not in config
assert b'value="OS User"' in resp.data
assert b'value="osuser"' in resp.data
def test_init_escapes_identity_values(self, journal_copy):
config = _read_config(journal_copy)
config.pop("setup", None)
- config["convey"].pop("password_hash", None)
config["identity"]["name"] = ""
(journal_copy / "config" / "journal.json").write_text(
json.dumps(config, indent=2)
@@ -325,7 +319,6 @@ class TestInitDetection:
def test_init_does_not_overwrite_existing_identity(self, journal_copy):
config = _read_config(journal_copy)
config.pop("setup", None)
- config["convey"].pop("password_hash", None)
config["identity"]["name"] = "Existing User"
config["identity"]["preferred"] = "Existing"
config["identity"]["timezone"] = "UTC"
@@ -429,8 +422,8 @@ class TestInitObservers:
assert isinstance(data["thresholds"]["active_ms"], int)
assert isinstance(data["thresholds"]["stale_ms"], int)
- def test_observers_no_password_required(self, fresh_client, monkeypatch):
- """Observers endpoint works without password_hash set."""
+ def test_observers_available_before_setup(self, fresh_client, monkeypatch):
+ """Observers endpoint works before setup completes."""
monkeypatch.setattr(
"solstone.apps.observer.utils.list_observers",
lambda: [],
@@ -479,7 +472,7 @@ class TestInitObservers:
assert observers[0]["elapsed_ms"] is None
assert observers[0]["clock_skew"] is False
- def test_init_observers_endpoint_parity(self, fresh_client):
+ def test_init_observers_endpoint_parity(self, fresh_client, journal_copy):
current_now = now_ms()
_save_test_observer(
"aaaa0000",
@@ -499,9 +492,12 @@ class TestInitObservers:
created_at=30,
last_seen=current_now - 600_000,
)
-
- with fresh_client.session_transaction() as sess:
- sess["logged_in"] = True
+ config = _read_config(journal_copy)
+ config["setup"] = {"completed_at": current_now}
+ (journal_copy / "config" / "journal.json").write_text(
+ json.dumps(config, indent=2),
+ encoding="utf-8",
+ )
api_resp = fresh_client.get("/app/observer/api/list")
init_resp = fresh_client.get("/init/observers")
@@ -535,7 +531,6 @@ class TestInitFinalize:
resp = fresh_client.post(
"/init/finalize",
json={
- "password": "securepass123",
"name": "Jane Doe",
"preferred": "Jane",
"timezone": "America/Denver",
@@ -549,12 +544,7 @@ class TestInitFinalize:
assert data["redirect"] == "/app/thinking/"
config = _read_config(journal_copy)
- # Password
- from werkzeug.security import check_password_hash
-
- assert check_password_hash(config["convey"]["password_hash"], "securepass123")
assert "allow_network_access" not in config["convey"]
- assert config["convey"]["trust_localhost"] is True
# Identity
assert config["identity"]["name"] == "Jane Doe"
assert config["identity"]["preferred"] == "Jane"
@@ -564,7 +554,7 @@ class TestInitFinalize:
# Setup
assert "completed_at" in config["setup"]
- def test_finalize_no_password_succeeds(self, fresh_client, journal_copy):
+ def test_finalize_succeeds(self, fresh_client, journal_copy):
resp = fresh_client.post(
"/init/finalize",
json={"name": "Jane"},
@@ -577,16 +567,6 @@ class TestInitFinalize:
config = _read_config(journal_copy)
assert "completed_at" in config["setup"]
assert "allow_network_access" not in config["convey"]
- assert config["convey"]["trust_localhost"] is True
- assert "password_hash" not in config["convey"]
-
- def test_finalize_password_too_short(self, fresh_client):
- resp = fresh_client.post(
- "/init/finalize",
- json={"password": "short"},
- content_type="application/json",
- )
- assert resp.status_code == 400
def test_finalize_minimal(self, fresh_client, journal_copy):
"""Finalize with optional fields omitted."""
@@ -597,7 +577,6 @@ class TestInitFinalize:
)
assert resp.status_code == 200
config = _read_config(journal_copy)
- assert "password_hash" not in config["convey"]
assert "completed_at" in config["setup"]
# No gemini key written
assert "GOOGLE_API_KEY" not in config.get("env", {})
@@ -646,25 +625,20 @@ class TestInitFinalize:
assert config["identity"]["timezone"] == "America/Denver"
assert "completed_at" in config["setup"]
- def test_finalize_auto_login(self, fresh_client, journal_copy):
+ def test_finalize_completes_setup_access(self, fresh_client, journal_copy):
response = fresh_client.post(
"/init/finalize",
json={},
content_type="application/json",
)
assert response.get_json()["redirect"] == "/app/thinking/"
- with fresh_client.session_transaction() as session:
- assert session["logged_in"] is True
- resp = fresh_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"})
+ resp = fresh_client.get("/")
assert resp.status_code == 302
- location = resp.headers["Location"]
- assert "/login" not in location
- assert "/init" not in location
+ assert resp.headers["Location"].endswith("/app/home/")
def test_finalize_no_early_config_write(self, fresh_client, journal_copy):
- """Before finalize, config should have no password_hash or setup."""
+ """Before finalize, config should have no setup."""
config = _read_config(journal_copy)
- assert "password_hash" not in config.get("convey", {})
assert "setup" not in config or "completed_at" not in config.get("setup", {})
def test_post_init_redirect(self, fresh_client, journal_copy):
@@ -722,189 +696,3 @@ class TestInitFinalize:
assert resp.get_json()["reason_code"] == "corrupt_config"
write_config.assert_not_called()
assert config_path.read_bytes() == before
-
-
-class TestRemovedEndpoints:
- """Verify old endpoints no longer exist."""
-
- def test_init_password_gone(self, fresh_client):
- resp = fresh_client.post(
- "/init/password",
- json={"password": "securepass123"},
- content_type="application/json",
- )
- assert resp.status_code in (404, 405)
-
- def test_init_identity_gone(self, fresh_client):
- resp = fresh_client.post(
- "/init/identity",
- json={"name": "Jane"},
- content_type="application/json",
- )
- assert resp.status_code in (404, 405)
-
- def test_init_provider_gone(self, fresh_client):
- resp = fresh_client.post(
- "/init/provider",
- json={"key": "some-key"},
- content_type="application/json",
- )
- assert resp.status_code in (404, 405)
-
-
-class TestLocalhostBypass:
- """Tests for the opt-in trust_localhost bypass."""
-
- def test_localhost_fresh_install_redirects_to_init(self, fresh_client):
- """Plain localhost with no config → redirect to /init."""
- resp = fresh_client.get("/")
- assert resp.status_code == 302
- assert "/init" in resp.headers["Location"]
-
- def test_localhost_trust_bypass(self, journal_copy):
- """Localhost + trust_localhost + setup.completed_at → pass through."""
- config = _read_config(journal_copy)
- config["convey"]["trust_localhost"] = True
- config["setup"] = {"completed_at": 1700000000000}
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
- app = create_app(str(journal_copy))
- app.config["TESTING"] = True
- client = app.test_client()
- resp = client.get("/")
- assert resp.status_code == 302
- # Should redirect to home app, not login or init
- assert "/login" not in resp.headers["Location"]
- assert "/init" not in resp.headers["Location"]
-
- def test_localhost_trust_without_setup_redirects_to_init(self, journal_copy):
- """trust_localhost set but no setup.completed_at → redirect to /init."""
- config = _read_config(journal_copy)
- config["convey"]["trust_localhost"] = True
- config.pop("setup", None)
- config["convey"].pop("password_hash", None)
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
- app = create_app(str(journal_copy))
- app.config["TESTING"] = True
- client = app.test_client()
- resp = client.get("/")
- assert resp.status_code == 302
- assert "/init" in resp.headers["Location"]
-
- def test_localhost_trust_disabled_redirects_to_login(self, journal_copy):
- """Localhost + setup.completed_at + trust_localhost false → redirect to /login."""
- config = _read_config(journal_copy)
- config["convey"]["trust_localhost"] = False
- config["setup"] = {"completed_at": 1700000000000}
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
- app = create_app(str(journal_copy))
- app.config["TESTING"] = True
- client = app.test_client()
- resp = client.get("/")
- assert resp.status_code == 302
- assert "/login" in resp.headers["Location"]
-
- def test_proxy_header_defeats_trust_localhost(self, configured_client):
- """Proxy headers prevent trust_localhost bypass."""
- resp = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"})
- assert resp.status_code == 302
- assert "/login" in resp.headers["Location"]
-
-
-class TestBasicAuth:
- """Tests for Basic Auth support."""
-
- def test_basic_auth_correct_password(self, configured_client):
- """Basic Auth with correct password → authenticated."""
- creds = base64.b64encode(b":test123").decode()
- resp = configured_client.get(
- "/",
- headers={
- "Authorization": f"Basic {creds}",
- "X-Forwarded-For": "1.2.3.4",
- },
- )
- assert resp.status_code == 302
- # Should redirect to home app, not login or init
- assert "/login" not in resp.headers["Location"]
- assert "/init" not in resp.headers["Location"]
-
- def test_basic_auth_wrong_password(self, configured_client):
- """Basic Auth with wrong password → redirect to /login."""
- creds = base64.b64encode(b":wrongpassword").decode()
- resp = configured_client.get(
- "/",
- headers={
- "Authorization": f"Basic {creds}",
- "X-Forwarded-For": "1.2.3.4",
- },
- )
- assert resp.status_code == 302
- assert "/login" in resp.headers["Location"]
-
- def test_basic_auth_no_session(self, configured_client):
- """Basic Auth does not create a session — next request without header fails."""
- creds = base64.b64encode(b":test123").decode()
- # First request with Basic Auth succeeds
- resp1 = configured_client.get(
- "/",
- headers={
- "Authorization": f"Basic {creds}",
- "X-Forwarded-For": "1.2.3.4",
- },
- )
- assert "/login" not in resp1.headers["Location"]
-
- # Second request without Basic Auth → should redirect to login
- resp2 = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"})
- assert resp2.status_code == 302
- assert "/login" in resp2.headers["Location"]
-
-
-class TestSetupMigration:
- """Tests for the _migrate_setup_completed migration.
-
- Legacy-only: handles journals where password was set via CLI before
- web onboarding existed. New onboarding writes all config atomically.
- """
-
- def test_migration_writes_setup_and_trust(self, journal_copy):
- """App startup with password_hash but no setup.completed_at writes both."""
- config = _read_config(journal_copy)
- config.pop("setup", None)
- config["convey"].pop("trust_localhost", None)
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
-
- # create_app triggers the migration
- create_app(str(journal_copy))
-
- config = _read_config(journal_copy)
- assert "completed_at" in config.get("setup", {})
- assert config["convey"].get("trust_localhost") is True
-
- def test_migration_idempotent(self, journal_copy):
- """Running migration twice is a no-op."""
- config = _read_config(journal_copy)
- config.pop("setup", None)
- config["convey"].pop("trust_localhost", None)
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
-
- # First run triggers migration
- create_app(str(journal_copy))
- config1 = _read_config(journal_copy)
- ts1 = config1["setup"]["completed_at"]
-
- # Second run should be a no-op
- create_app(str(journal_copy))
- config2 = _read_config(journal_copy)
- assert config2["setup"]["completed_at"] == ts1
- assert config2["convey"]["trust_localhost"] is True
diff --git a/tests/test_ledger_call_parity.py b/tests/test_ledger_call_parity.py
index 50a736fdd..1c50b7e7c 100644
--- a/tests/test_ledger_call_parity.py
+++ b/tests/test_ledger_call_parity.py
@@ -15,7 +15,7 @@ from solstone.convey.reasons import ACTIVITIES_BUSY
from solstone.think.convey_client import ConveyClient
from solstone.think.journal_io import LockTimeout
from solstone.think.tools.ledger import app
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
from tests.test_surfaces_ledger import (
_commitment,
_decision,
@@ -28,12 +28,13 @@ from tests.test_surfaces_ledger import (
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.think.tools.ledger.get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_ledger_routes.py b/tests/test_ledger_routes.py
index a9bd8fba3..cc2e3f0f6 100644
--- a/tests/test_ledger_routes.py
+++ b/tests/test_ledger_routes.py
@@ -3,8 +3,10 @@
from __future__ import annotations
+import json
+
from solstone.convey import create_app
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
from tests.test_surfaces_ledger import (
_commitment,
_minimal_facet_tree,
@@ -26,6 +28,16 @@ def _assert_error(response, status: int) -> dict:
def _configure_journal(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ config_dir = tmp_path / "config"
+ config_dir.mkdir(parents=True, exist_ok=True)
+ (config_dir / "journal.json").write_text(
+ json.dumps({"setup": {"completed_at": 1}}),
+ encoding="utf-8",
+ )
+
+
+def _configure_unset_journal(tmp_path, monkeypatch) -> None:
+ monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
def _decision(
@@ -54,7 +66,7 @@ def test_ledger_list_collection_envelope_and_bound(tmp_path, monkeypatch):
_utc_ms(f"2026-04-10T09:{index:02d}:00Z"),
commitments=[_commitment(action=f"action number {index}")],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(PREFIX)
@@ -90,7 +102,7 @@ def test_ledger_list_counterparty_filter_narrows_total(tmp_path, monkeypatch):
)
],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
all_response = client.get(PREFIX)
filtered_response = client.get(f"{PREFIX}?counterparty=Ravi")
@@ -103,7 +115,7 @@ def test_ledger_list_counterparty_filter_narrows_total(tmp_path, monkeypatch):
def test_ledger_list_bad_state_returns_invalid_request_value(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}?state=bogus"), 400)
@@ -113,7 +125,7 @@ def test_ledger_list_bad_state_returns_invalid_request_value(tmp_path, monkeypat
def test_ledger_list_bad_sort_returns_invalid_request_value(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}?sort=bogus"), 400)
@@ -125,7 +137,7 @@ def test_ledger_list_bad_age_days_gte_returns_invalid_request_value(
):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}?age_days_gte=abc"), 400)
@@ -135,7 +147,7 @@ def test_ledger_list_bad_age_days_gte_returns_invalid_request_value(
def test_ledger_list_bad_closed_since_returns_invalid_day(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
notaday = _assert_error(client.get(f"{PREFIX}?closed_since=notaday"), 400)
bad_month = _assert_error(client.get(f"{PREFIX}?closed_since=20261301"), 400)
@@ -154,7 +166,7 @@ def test_ledger_decisions_collection_envelope_and_routing(tmp_path, monkeypatch)
_utc_ms("2026-04-10T09:00:00Z"),
decisions=[_decision(action="move launch review")],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PREFIX}/decisions")
@@ -168,7 +180,7 @@ def test_ledger_decisions_collection_envelope_and_routing(tmp_path, monkeypatch)
def test_ledger_decisions_bad_since_returns_invalid_day(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}/decisions?since=bad"), 400)
@@ -185,7 +197,7 @@ def test_ledger_get_item_returns_recursive_dataclass_dict(tmp_path, monkeypatch)
_utc_ms("2026-04-10T09:00:00Z"),
commitments=[_commitment()],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
item_id = client.get(PREFIX).get_json()["items"][0]["id"]
response = client.get(f"{PREFIX}/{item_id}")
@@ -200,7 +212,7 @@ def test_ledger_get_item_returns_recursive_dataclass_dict(tmp_path, monkeypatch)
def test_ledger_get_item_missing_returns_ledger_item_not_found(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PREFIX}/does-not-exist"), 404)
@@ -217,7 +229,7 @@ def test_ledger_close_flips_state(tmp_path, monkeypatch):
_utc_ms("2026-04-10T09:00:00Z"),
commitments=[_commitment()],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
item_id = client.get(PREFIX).get_json()["items"][0]["id"]
closed_response = client.post(f"{PREFIX}/{item_id}/close", json={"note": "done"})
@@ -242,7 +254,7 @@ def test_ledger_close_decision_id_returns_ledger_item_not_found(tmp_path, monkey
_utc_ms("2026-04-10T09:00:00Z"),
decisions=[_decision()],
)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
decision_id = client.get(f"{PREFIX}/decisions").get_json()["items"][0]["id"]
data = _assert_error(
@@ -255,7 +267,7 @@ def test_ledger_close_decision_id_returns_ledger_item_not_found(tmp_path, monkey
def test_ledger_close_no_body_returns_missing_request_body(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.post(f"{PREFIX}/does-not-exist/close"), 400)
@@ -265,7 +277,7 @@ def test_ledger_close_no_body_returns_missing_request_body(tmp_path, monkeypatch
def test_ledger_close_non_json_returns_invalid_json_request(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(
client.post(
@@ -282,7 +294,7 @@ def test_ledger_close_non_json_returns_invalid_json_request(tmp_path, monkeypatc
def test_ledger_close_empty_note_returns_missing_required_field(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(
client.post(f"{PREFIX}/does-not-exist/close", json={"note": " "}), 400
@@ -294,7 +306,7 @@ def test_ledger_close_empty_note_returns_missing_required_field(tmp_path, monkey
def test_ledger_close_bad_as_state_returns_invalid_request_value(tmp_path, monkeypatch):
_configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(
client.post(
@@ -307,8 +319,8 @@ def test_ledger_close_bad_as_state_returns_invalid_request_value(tmp_path, monke
assert data["reason_code"] == "invalid_request_value"
-def test_ledger_requires_login(tmp_path, monkeypatch):
- _configure_journal(tmp_path, monkeypatch)
+def test_ledger_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch):
+ _configure_unset_journal(tmp_path, monkeypatch)
app = create_app(journal=str(tmp_path))
app.config["TESTING"] = True
client = app.test_client()
@@ -316,3 +328,4 @@ def test_ledger_requires_login(tmp_path, monkeypatch):
response = client.get(PREFIX)
assert response.status_code == 302
+ assert "/init" in response.headers["Location"]
diff --git a/tests/test_link_call_parity.py b/tests/test_link_call_parity.py
index 479d6bb46..27f3d67ee 100644
--- a/tests/test_link_call_parity.py
+++ b/tests/test_link_call_parity.py
@@ -18,7 +18,7 @@ from solstone.think.link.paths import (
authorized_clients_path,
nonces_path,
)
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
PAIRED_AT = "2026-04-19T00:00:00Z"
LAST_SEEN_AT = "2026-04-19T00:30:00Z"
@@ -33,7 +33,6 @@ def journal(tmp_path, monkeypatch):
(config_dir / "journal.json").write_text(
json.dumps(
{
- "convey": {"trust_localhost": True},
"setup": {"completed_at": 1700000000000},
},
indent=2,
@@ -45,7 +44,7 @@ def journal(tmp_path, monkeypatch):
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.link.call.get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_password.py b/tests/test_password.py
deleted file mode 100644
index dc776b0c0..000000000
--- a/tests/test_password.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright (c) 2026 sol pbc
-
-"""Tests for password hashing: login, migration, and settings API."""
-
-from __future__ import annotations
-
-import json
-from pathlib import Path
-
-import pytest
-from werkzeug.security import check_password_hash
-
-from solstone.convey import create_app
-from tests.conftest import copytree_tracked
-
-
-@pytest.fixture
-def client(journal_copy):
- app = create_app(str(journal_copy))
- app.config["TESTING"] = True
- return app.test_client()
-
-
-def _read_config(journal_dir):
- return json.loads((journal_dir / "config" / "journal.json").read_text())
-
-
-class TestLogin:
- def test_correct_password(self, client):
- resp = client.post("/login", data={"password": "test123"})
- assert resp.status_code == 302
-
- def test_wrong_password(self, client):
- resp = client.post("/login", data={"password": "wrong"})
- assert resp.status_code == 200
- assert b"incorrect password. passwords are case-sensitive." in resp.data
-
- def test_no_password_configured(self, journal_copy):
- config = _read_config(journal_copy)
- config["convey"].pop("password_hash", None)
- config["convey"].pop("password", None)
- (journal_copy / "config" / "journal.json").write_text(
- json.dumps(config, indent=2)
- )
- app = create_app(str(journal_copy))
- app.config["TESTING"] = True
- client = app.test_client()
- resp = client.get("/login")
- assert b"journal password set" in resp.data
-
-
-class TestMigration:
- def test_plaintext_migrated_to_hash(self, tmp_path, monkeypatch):
- """Plaintext password is hashed and old key removed on app creation."""
- src = Path(__file__).resolve().parent / "fixtures" / "journal"
- dst = tmp_path / "journal"
- copytree_tracked(src, dst)
- config_path = dst / "config" / "journal.json"
- config = json.loads(config_path.read_text())
- config["convey"].pop("password_hash", None)
- config["convey"]["password"] = "migrate-me"
- config_path.write_text(json.dumps(config, indent=2))
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst))
-
- create_app(str(dst))
-
- config = json.loads(config_path.read_text())
- assert "password" not in config["convey"]
- assert "password_hash" in config["convey"]
- assert check_password_hash(config["convey"]["password_hash"], "migrate-me")
-
- def test_empty_password_removed(self, tmp_path, monkeypatch):
- """Empty plaintext password is removed, not hashed."""
- src = Path(__file__).resolve().parent / "fixtures" / "journal"
- dst = tmp_path / "journal"
- copytree_tracked(src, dst)
- config_path = dst / "config" / "journal.json"
- config = json.loads(config_path.read_text())
- config["convey"].pop("password_hash", None)
- config["convey"]["password"] = ""
- config_path.write_text(json.dumps(config, indent=2))
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst))
-
- create_app(str(dst))
-
- config = json.loads(config_path.read_text())
- assert "password" not in config["convey"]
- assert "password_hash" not in config["convey"]
-
- def test_already_migrated_skipped(self, journal_copy):
- """If password_hash exists, migration is a no-op."""
- config_before = _read_config(journal_copy)
- hash_before = config_before["convey"]["password_hash"]
-
- create_app(str(journal_copy))
-
- config_after = _read_config(journal_copy)
- assert config_after["convey"]["password_hash"] == hash_before
-
-
-class TestSettingsAPI:
- def test_get_config_strips_password(self, client):
- """GET /app/settings/api/config must not return password or password_hash."""
- resp = client.get("/app/settings/api/config")
- data = resp.get_json()
- convey = data.get("convey", {})
- assert "password" not in convey
- assert "password_hash" not in convey
- assert convey.get("has_password") is True
-
- def test_put_hashes_password(self, client, journal_copy):
- """PUT with convey.password hashes before writing to disk."""
- resp = client.put(
- "/app/settings/api/config",
- json={"section": "convey", "data": {"password": "new-secret"}},
- content_type="application/json",
- )
- assert resp.status_code == 200
- config = _read_config(journal_copy)
- assert "password" not in config["convey"]
- assert check_password_hash(config["convey"]["password_hash"], "new-secret")
-
- def test_put_empty_password_skipped(self, client, journal_copy):
- """PUT with empty password does not overwrite existing hash."""
- config_before = _read_config(journal_copy)
- hash_before = config_before["convey"]["password_hash"]
-
- resp = client.put(
- "/app/settings/api/config",
- json={"section": "convey", "data": {"password": ""}},
- content_type="application/json",
- )
- assert resp.status_code == 200
- config_after = _read_config(journal_copy)
- assert config_after["convey"]["password_hash"] == hash_before
diff --git a/tests/test_password_cli.py b/tests/test_password_cli.py
deleted file mode 100644
index d63da7901..000000000
--- a/tests/test_password_cli.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright (c) 2026 sol pbc
-
-"""Tests for the journal password CLI."""
-
-from __future__ import annotations
-
-import json
-
-import pytest
-from werkzeug.security import check_password_hash
-
-from solstone.think.password_cli import main
-
-
-def _read_config(journal_dir):
- return json.loads((journal_dir / "config" / "journal.json").read_text())
-
-
-def _mock_getpass(monkeypatch, *responses):
- """Mock getpass.getpass to return successive responses."""
- it = iter(responses)
- monkeypatch.setattr(
- "solstone.think.password_cli.getpass.getpass", lambda prompt="": next(it)
- )
-
-
-class TestSetPassword:
- def test_set_writes_hash(self, journal_copy, monkeypatch, capsys):
- monkeypatch.setattr("sys.argv", ["sol password", "set"])
- _mock_getpass(monkeypatch, "mypassword", "mypassword")
-
- main()
-
- config = _read_config(journal_copy)
- assert config["convey"]["password_hash"].startswith("scrypt:")
- assert check_password_hash(config["convey"]["password_hash"], "mypassword")
- assert "Password set successfully." in capsys.readouterr().out
-
- def test_mismatch_rejected(self, journal_copy, monkeypatch, capsys):
- monkeypatch.setattr("sys.argv", ["sol password", "set"])
- _mock_getpass(monkeypatch, "password1", "different")
-
- with pytest.raises(SystemExit) as exc_info:
- main()
-
- assert exc_info.value.code == 1
- assert "Passwords do not match." in capsys.readouterr().err
-
- def test_plaintext_cleanup(self, journal_copy, monkeypatch):
- # Seed a plaintext password
- config_path = journal_copy / "config" / "journal.json"
- config = json.loads(config_path.read_text())
- config["convey"]["password"] = "old-plaintext"
- config_path.write_text(json.dumps(config, indent=2))
-
- monkeypatch.setattr("sys.argv", ["sol password", "set"])
- _mock_getpass(monkeypatch, "newpass", "newpass")
-
- main()
-
- config = _read_config(journal_copy)
- assert "password" not in config["convey"]
- assert "password_hash" in config["convey"]
-
- def test_no_config_file(self, tmp_path, monkeypatch, capsys):
- """Works when no journal.json exists yet."""
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- monkeypatch.setattr("sys.argv", ["sol password", "set"])
- _mock_getpass(monkeypatch, "freshpass", "freshpass")
-
- main()
-
- config_path = tmp_path / "config" / "journal.json"
- assert config_path.exists()
- config = json.loads(config_path.read_text())
- assert check_password_hash(config["convey"]["password_hash"], "freshpass")
- assert config_path.stat().st_mode & 0o777 == 0o600
-
- def test_file_permissions(self, journal_copy, monkeypatch):
- monkeypatch.setattr("sys.argv", ["sol password", "set"])
- _mock_getpass(monkeypatch, "securepass", "securepass")
-
- main()
-
- config_path = journal_copy / "config" / "journal.json"
- assert config_path.stat().st_mode & 0o777 == 0o600
-
-
-class TestResetAlias:
- def test_reset_writes_hash(self, journal_copy, monkeypatch, capsys):
- monkeypatch.setattr("sys.argv", ["sol password", "reset"])
- _mock_getpass(monkeypatch, "resetpass", "resetpass")
-
- main()
-
- config = _read_config(journal_copy)
- assert check_password_hash(config["convey"]["password_hash"], "resetpass")
- assert "Password set successfully." in capsys.readouterr().out
diff --git a/tests/test_profile_call_parity.py b/tests/test_profile_call_parity.py
index ddcdddba1..02ad91ac0 100644
--- a/tests/test_profile_call_parity.py
+++ b/tests/test_profile_call_parity.py
@@ -13,7 +13,7 @@ from typer.testing import CliRunner
from solstone.think.convey_client import ConveyClient
from solstone.think.surfaces import profile as profile_surface
from solstone.think.tools.profile import app
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
from tests.test_surfaces_profile import (
_activity_record,
_append_activity,
@@ -29,12 +29,13 @@ from tests.test_surfaces_profile import (
@pytest.fixture
def journal(tmp_path, monkeypatch):
_configure_env(tmp_path, monkeypatch)
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.think.tools.profile.get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py
index 34578caa5..81e0dcc96 100644
--- a/tests/test_profile_routes.py
+++ b/tests/test_profile_routes.py
@@ -3,9 +3,11 @@
from __future__ import annotations
+import json
+
from solstone.convey import create_app
from solstone.think.surfaces import profile as profile_surface
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
from tests.test_surfaces_profile import (
_activity_record,
_append_activity,
@@ -30,8 +32,22 @@ def _assert_error(response, status: int) -> dict:
return data
-def _seed_ravi(tmp_path, monkeypatch) -> None:
+def _configure_journal(tmp_path, monkeypatch) -> None:
+ monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ config_dir = tmp_path / "config"
+ config_dir.mkdir(parents=True, exist_ok=True)
+ (config_dir / "journal.json").write_text(
+ json.dumps({"setup": {"completed_at": 1}}),
+ encoding="utf-8",
+ )
+
+
+def _configure_unset_journal(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+
+
+def _seed_ravi(tmp_path, monkeypatch) -> None:
+ _configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(
tmp_path,
journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
@@ -58,7 +74,7 @@ def _seed_ravi(tmp_path, monkeypatch) -> None:
def test_profile_full_composed(tmp_path, monkeypatch):
_seed_ravi(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PROFILE_PREFIX}/Ravi")
@@ -72,10 +88,10 @@ def test_profile_full_composed(tmp_path, monkeypatch):
def test_profile_full_not_found(tmp_path, monkeypatch):
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ _configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260607")
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PROFILE_PREFIX}/missing"), 404)
@@ -84,7 +100,7 @@ def test_profile_full_not_found(tmp_path, monkeypatch):
def test_profile_brief(tmp_path, monkeypatch):
_seed_ravi(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PROFILE_PREFIX}/Ravi/brief")
@@ -94,7 +110,7 @@ def test_profile_brief(tmp_path, monkeypatch):
def test_profile_cadence(tmp_path, monkeypatch):
_seed_ravi(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PROFILE_PREFIX}/Ravi/cadence")
mentions_response = client.get(
@@ -108,7 +124,7 @@ def test_profile_cadence(tmp_path, monkeypatch):
def test_profiles_active_collection_envelope(tmp_path, monkeypatch):
_seed_ravi(tmp_path, monkeypatch)
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
response = client.get(f"{PROFILES_PREFIX}/active")
@@ -123,18 +139,18 @@ def test_profiles_active_collection_envelope(tmp_path, monkeypatch):
def test_profiles_active_bad_window_days_returns_invalid_request_value(
tmp_path, monkeypatch
):
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ _configure_journal(tmp_path, monkeypatch)
_minimal_facet_tree(tmp_path)
monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260607")
- client = make_logged_in_test_client(tmp_path)
+ client = make_test_client(tmp_path)
data = _assert_error(client.get(f"{PROFILES_PREFIX}/active?window_days=abc"), 400)
assert data["reason_code"] == "invalid_request_value"
-def test_profile_requires_login(tmp_path, monkeypatch):
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+def test_profile_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch):
+ _configure_unset_journal(tmp_path, monkeypatch)
app = create_app(journal=str(tmp_path))
app.config["TESTING"] = True
client = app.test_client()
@@ -142,3 +158,4 @@ def test_profile_requires_login(tmp_path, monkeypatch):
response = client.get(f"{PROFILE_PREFIX}/Ravi")
assert response.status_code == 302
+ assert "/init" in response.headers["Location"]
diff --git a/tests/test_push_routes.py b/tests/test_push_routes.py
index e10aca315..69b444ed8 100644
--- a/tests/test_push_routes.py
+++ b/tests/test_push_routes.py
@@ -132,12 +132,10 @@ def test_register_refuses_none_fingerprint_on_loopback(journal_copy):
assert response.get_json()["reason_code"] == "push_request_invalid"
-def test_register_refuses_none_fingerprint_on_session_auth(journal_copy):
+def test_register_refuses_missing_paired_identity(journal_copy):
app = create_app(str(journal_copy))
app.config["TESTING"] = True
client = app.test_client()
- with client.session_transaction() as session:
- session["logged_in"] = True
try:
response = client.post(
"/api/push/register",
diff --git a/tests/test_settings_call_parity.py b/tests/test_settings_call_parity.py
index 6cea6c927..f5f6c63b3 100644
--- a/tests/test_settings_call_parity.py
+++ b/tests/test_settings_call_parity.py
@@ -14,7 +14,7 @@ from typer.testing import CliRunner
import solstone.apps.settings.call as settings_call
import solstone.apps.settings.routes as settings_routes
from solstone.think.convey_client import ConveyClient
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
runner = CliRunner()
@@ -39,7 +39,7 @@ def _settings_client(journal_copy: Path, monkeypatch: pytest.MonkeyPatch) -> Non
for key in API_ENV_KEYS:
monkeypatch.delenv(key, raising=False)
client = ConveyClient(
- session=make_logged_in_test_client(journal_copy),
+ session=make_test_client(journal_copy),
base_url="",
)
monkeypatch.setattr(settings_call, "get_client", lambda: client)
@@ -307,7 +307,7 @@ def test_identity_and_observer_setters(journal_copy: Path) -> None:
}
-def test_convey_status_host_url_and_trust_localhost(
+def test_convey_status_host_url(
journal_copy: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -316,8 +316,6 @@ def test_convey_status_host_url_and_trust_localhost(
assert status.stdout == (
"convey\n"
" bind: 127.0.0.1:5015\n"
- " password: set\n"
- " trust localhost: yes\n"
" host url: http://localhost:5015\n"
)
@@ -364,33 +362,3 @@ def test_convey_status_host_url_and_trust_localhost(
"this needs an ip address — to reach home by name from anywhere, "
"set up solstone private link\n"
)
-
- trust_enable = runner.invoke(
- settings_call.app, ["convey", "trust-localhost", "enable"]
- )
- assert trust_enable.exit_code == 0
- assert (
- trust_enable.stdout
- == "localhost trust enabled. localhost requests skip the password.\n"
- )
-
- trust_disable = runner.invoke(
- settings_call.app, ["convey", "trust-localhost", "disable"]
- )
- assert trust_disable.exit_code == 0
- assert trust_disable.stdout == (
- "localhost trust disabled. localhost requests now require the password.\n"
- )
-
- config = _read_config(journal_copy)
- config["convey"].pop("password_hash", None)
- config["convey"].pop("password", None)
- _write_config(journal_copy, config)
- trust_refuse = runner.invoke(
- settings_call.app, ["convey", "trust-localhost", "disable"]
- )
- assert trust_refuse.exit_code == 1
- assert trust_refuse.stderr == (
- "error: disabling localhost trust requires a password (otherwise no "
- "client could authenticate). set one first with: journal password set\n"
- )
diff --git a/tests/test_settings_cli.py b/tests/test_settings_cli.py
index a9ae5c80b..fc346ebfb 100644
--- a/tests/test_settings_cli.py
+++ b/tests/test_settings_cli.py
@@ -29,7 +29,6 @@ def test_status_json_does_not_require_running_stack(
payload = json.loads(captured.out)
assert payload == {
"effective_host_url": "http://localhost:5015",
- "password_configured": True,
}
assert captured.err == ""
diff --git a/tests/test_settings_convey.py b/tests/test_settings_convey.py
deleted file mode 100644
index 687ea8d39..000000000
--- a/tests/test_settings_convey.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright (c) 2026 sol pbc
-
-from __future__ import annotations
-
-import json
-from pathlib import Path
-from unittest.mock import patch
-
-from solstone.apps.settings.copy import (
- CONVEY_REFUSE_NO_PASSWORD_TRUST,
-)
-from solstone.convey import create_app
-
-
-def _read_config(journal_dir: Path) -> dict:
- return json.loads((journal_dir / "config" / "journal.json").read_text("utf-8"))
-
-
-def _write_config(journal_dir: Path, payload: dict) -> None:
- (journal_dir / "config" / "journal.json").write_text(
- json.dumps(payload, indent=2) + "\n",
- encoding="utf-8",
- )
-
-
-def _clear_password(journal_dir: Path) -> None:
- config = _read_config(journal_dir)
- config["convey"].pop("password_hash", None)
- config["convey"].pop("password", None)
- _write_config(journal_dir, config)
-
-
-def _settings_client(journal_dir: Path):
- app = create_app(str(journal_dir))
- app.config["TESTING"] = True
- return app.test_client()
-
-
-def test_api_get_config_masks_password_without_effective_host_url(journal_copy):
- client = _settings_client(journal_copy)
-
- response = client.get("/app/settings/api/config")
-
- assert response.status_code == 200
- payload = response.get_json()
- assert payload["convey"]["allow_network_access"] is False
- assert payload["convey"]["has_password"] is True
- assert "password_hash" not in payload["convey"]
- assert "pairing" not in payload
-
-
-def test_api_put_corrupt_config_returns_reason_without_writing(journal_copy):
- client = _settings_client(journal_copy)
- with client.session_transaction() as sess:
- sess["logged_in"] = True
- config_path = journal_copy / "config" / "journal.json"
- config_path.write_bytes(b"{ invalid json }")
- before = config_path.read_bytes()
-
- with patch("solstone.apps.settings.routes.write_journal_config") as write_config:
- response = client.put(
- "/app/settings/api/config",
- json={"section": "identity", "data": {"name": "Changed"}},
- content_type="application/json",
- )
-
- assert response.status_code == 500
- assert response.get_json()["reason_code"] == "corrupt_config"
- write_config.assert_not_called()
- assert config_path.read_bytes() == before
-
-
-def test_api_put_trust_localhost_refuses_without_password(journal_copy):
- _clear_password(journal_copy)
- client = _settings_client(journal_copy)
-
- response = client.put(
- "/app/settings/api/config",
- json={"section": "convey", "data": {"trust_localhost": False}},
- content_type="application/json",
- )
-
- assert response.status_code == 400
- payload = response.get_json()
- assert (
- payload["error"] == "I couldn't change localhost trust until a password is set."
- )
- assert payload["reason_code"] == "network_security_requires_password"
- assert payload["detail"] == CONVEY_REFUSE_NO_PASSWORD_TRUST
diff --git a/tests/test_setup.py b/tests/test_setup.py
index 8d7f62a81..2d08ec1d9 100644
--- a/tests/test_setup.py
+++ b/tests/test_setup.py
@@ -586,7 +586,7 @@ def test_step_journal_materializes_journal_config(
assert config["identity"]["name"] == "Setup User"
assert config["identity"]["preferred"] == "setup"
assert config["identity"]["timezone"] == "America/Denver"
- assert config["convey"]["secret"]
+ assert "convey" not in config
assert str(config_path.resolve()) in result.paths
diff --git a/tests/test_sol_call_parity.py b/tests/test_sol_call_parity.py
index e71a6b553..bc2bff270 100644
--- a/tests/test_sol_call_parity.py
+++ b/tests/test_sol_call_parity.py
@@ -13,24 +13,25 @@ from typer.testing import CliRunner
from solstone.apps.sol.call import app
from solstone.think.convey_client import ConveyClient
from solstone.think.journal_config import read_journal_config, write_journal_config
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
@pytest.fixture
def journal(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal, monkeypatch):
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr("solstone.apps.sol.call.get_client", lambda: client)
return CliRunner()
def test_set_name_updates_config(runner) -> None:
- write_journal_config({})
+ write_journal_config({"setup": {"completed_at": 1700000000000}})
result = runner.invoke(app, ["set-name", "aria", "--status", "chosen"])
@@ -45,11 +46,12 @@ def test_set_name_updates_config(runner) -> None:
def test_reset_updates_agent(runner) -> None:
write_journal_config(
{
+ "setup": {"completed_at": 1700000000000},
"agent": {
"name": "aria",
"name_status": "chosen",
"named_date": "2026-04-19",
- }
+ },
}
)
@@ -65,7 +67,7 @@ def test_reset_updates_agent(runner) -> None:
def test_set_owner_name_only_and_bio(runner) -> None:
- write_journal_config({})
+ write_journal_config({"setup": {"completed_at": 1700000000000}})
name_only = runner.invoke(app, ["set-owner", "Jer"])
with_bio = runner.invoke(app, ["set-owner", "Jer", "--bio", "Building solstone"])
diff --git a/tests/test_speakers_call_parity.py b/tests/test_speakers_call_parity.py
index 3986925ea..36159d2ee 100644
--- a/tests/test_speakers_call_parity.py
+++ b/tests/test_speakers_call_parity.py
@@ -17,18 +17,19 @@ from solstone.apps.speakers.call import app
from solstone.convey.reasons import SPEAKER_LABELS_BUSY, SPEAKER_VOICEPRINT_BUSY
from solstone.think.convey_client import ConveyClient
from solstone.think.journal_io import LockTimeout
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client, mark_setup_complete
@pytest.fixture
def journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ mark_setup_complete(tmp_path)
return tmp_path
@pytest.fixture
def runner(journal: Path, monkeypatch: pytest.MonkeyPatch) -> CliRunner:
- client = ConveyClient(session=make_logged_in_test_client(journal), base_url="")
+ client = ConveyClient(session=make_test_client(journal), base_url="")
monkeypatch.setattr(speakers_call, "get_client", lambda: client)
return CliRunner()
diff --git a/tests/test_surfaces_health.py b/tests/test_surfaces_health.py
index af7390ef5..db9273bc3 100644
--- a/tests/test_surfaces_health.py
+++ b/tests/test_surfaces_health.py
@@ -17,7 +17,7 @@ from solstone.convey.readiness_snapshot import unavailable_snapshot
from solstone.think.convey_client import ConveyClient
from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion
from solstone.think.surfaces import health as health_surface
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
_RUNNER = CliRunner()
_SPEC_POINTER = "cpo/specs/in-flight/consumer-surface-health.md"
@@ -26,6 +26,12 @@ _SPEC_POINTER = "cpo/specs/in-flight/consumer-surface-health.md"
def _configure_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")
+ config_dir = tmp_path / "config"
+ config_dir.mkdir(parents=True, exist_ok=True)
+ (config_dir / "journal.json").write_text(
+ json.dumps({"setup": {"completed_at": 1}}),
+ encoding="utf-8",
+ )
def _set_now(monkeypatch: pytest.MonkeyPatch, value: datetime) -> None:
@@ -278,7 +284,7 @@ def _neutral_readiness_snapshot() -> dict[str, object]:
def _patch_health_cli_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- client = ConveyClient(session=make_logged_in_test_client(tmp_path), base_url="")
+ client = ConveyClient(session=make_test_client(tmp_path), base_url="")
monkeypatch.setattr("solstone.think.tools.health.get_client", lambda: client)
diff --git a/tests/test_system_status.py b/tests/test_system_status.py
index 13b17be5f..b8e95eb00 100644
--- a/tests/test_system_status.py
+++ b/tests/test_system_status.py
@@ -30,7 +30,6 @@ def client(tmp_path, monkeypatch):
journal = tmp_path
(journal / "config").mkdir(parents=True, exist_ok=True)
config = {
- "convey": {"password_hash": "", "trust_localhost": True},
"setup": {"completed_at": 1},
}
(journal / "config" / "journal.json").write_text(json.dumps(config))
diff --git a/tests/test_thinking_call_parity.py b/tests/test_thinking_call_parity.py
index bff2d8632..5a2d02f9c 100644
--- a/tests/test_thinking_call_parity.py
+++ b/tests/test_thinking_call_parity.py
@@ -16,7 +16,7 @@ import solstone.apps.thinking.routes as thinking_routes
from solstone.apps.thinking import copy as thinking_copy
from solstone.think.convey_client import ConveyClient
from solstone.think.services import operations, scout, scout_handoff
-from tests._baseline_harness import make_logged_in_test_client
+from tests._baseline_harness import make_test_client
runner = CliRunner()
@@ -41,7 +41,7 @@ def _thinking_client(journal_copy: Path, monkeypatch: pytest.MonkeyPatch) -> Non
for key in API_ENV_KEYS:
monkeypatch.delenv(key, raising=False)
client = ConveyClient(
- session=make_logged_in_test_client(journal_copy),
+ session=make_test_client(journal_copy),
base_url="",
)
monkeypatch.setattr(thinking_call, "get_client", lambda: client)
diff --git a/tests/verify_api.py b/tests/verify_api.py
index 0c6e0e690..cb20e4a3b 100644
--- a/tests/verify_api.py
+++ b/tests/verify_api.py
@@ -22,7 +22,7 @@ try:
FROZEN_DATE,
FROZEN_TZ_OFFSET,
isolated_app_env,
- make_logged_in_test_client,
+ make_test_client,
prepare_isolated_journal,
)
except ModuleNotFoundError:
@@ -30,7 +30,7 @@ except ModuleNotFoundError:
FROZEN_DATE,
FROZEN_TZ_OFFSET,
isolated_app_env,
- make_logged_in_test_client,
+ make_test_client,
prepare_isolated_journal,
)
@@ -699,7 +699,7 @@ def client_context(
journal_path = prepare_isolated_journal(Path(tmpdir) / "journal")
with freeze_time(FROZEN_DATE, tz_offset=FROZEN_TZ_OFFSET):
with isolated_app_env(journal_path):
- yield make_logged_in_test_client(journal_path), str(journal_path)
+ yield make_test_client(journal_path), str(journal_path)
def main(argv: list[str] | None = None) -> int: