Skip to content

Commit 7e593d2

Browse files
Brian KrafftCopilot
andcommitted
fix: address R1 review findings for Epic 6.4 DX polish - preflight visibility, platform-aware suggestions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e1e2165 commit 7e593d2

4 files changed

Lines changed: 94 additions & 18 deletions

File tree

RUNBOOK.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
schema_version: '1.0'
22
lock:
3-
active_claim: null
4-
claimed_by: null
3+
active_claim: '6.4'
4+
claimed_by: 2026-04-07-e9b4572b
55
claimed_at: '2026-04-07T01:20:00Z'
66
review:
77
max_rounds: 3
@@ -585,10 +585,10 @@ epics:
585585
- id: '6.4'
586586
phase: P6
587587
title: DX polish (CLI help, errors, onboarding)
588-
status: pending
589-
branch: null
590-
pr: null
591-
review_round: 0
588+
status: claimed
589+
branch: epic/6.4-dx-polish
590+
pr: 53
591+
review_round: 1
592592
description: 'Improve CLI help text, error messages with actionable suggestions,
593593
first-run onboarding flow, quickstart documentation.
594594

openspace/deploy/preflight.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,20 @@ def check_bearer_token(transport: str) -> Optional[PreflightIssue]:
6060
token_env = "OPENSPACE_MCP_BEARER_TOKEN"
6161
token = os.environ.get(token_env, "").strip()
6262
if not token:
63+
if sys.platform == "win32":
64+
set_cmd = (
65+
f'$env:{token_env} = python -c "import secrets; print(secrets.token_urlsafe(32))"'
66+
)
67+
else:
68+
set_cmd = (
69+
f'export {token_env}=$(python -c "import secrets; print(secrets.token_urlsafe(32))")'
70+
)
6371
return PreflightIssue(
6472
check="bearer-token",
6573
message=f"{token_env} not set (required for {transport} transport)",
6674
suggestion=(
6775
f"Set {token_env} to a strong random token:\n"
68-
f" export {token_env}=$(python -c \"import secrets; print(secrets.token_urlsafe(32))\")\n"
76+
f" {set_cmd}\n"
6977
f" Or use --transport stdio for local-only access (no auth required)"
7078
),
7179
)
@@ -76,10 +84,14 @@ def check_skill_store(path: str) -> Optional[PreflightIssue]:
7684
"""Verify skill store directory exists or can be created."""
7785
p = Path(path)
7886
if p.exists() and not p.is_dir():
87+
if sys.platform == "win32":
88+
fix_cmd = f"Remove-Item {path}; New-Item -ItemType Directory {path}"
89+
else:
90+
fix_cmd = f"rm {path} && mkdir -p {path}"
7991
return PreflightIssue(
8092
check="skill-store",
8193
message=f"Skill store path exists but is not a directory: {path}",
82-
suggestion=f"Remove the file and create directory:\n rm {path} && mkdir -p {path}",
94+
suggestion=f"Remove the file and create directory:\n {fix_cmd}",
8395
)
8496
return None
8597

openspace/mcp/server.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,10 @@ def run_mcp_server(mcp=None) -> None:
308308
if issues:
309309
errors = [i for i in issues if i.severity == "error"]
310310
report = format_preflight_report(issues)
311-
logger.info(report)
311+
# Write directly to console — logger goes to file only, user won't see it
312+
_console = sys.__stderr__ or sys.stderr
313+
_console.write(report + "\n")
314+
_console.flush()
312315
if errors:
313316
sys.exit(1)
314317

@@ -318,19 +321,25 @@ def run_mcp_server(mcp=None) -> None:
318321

319322
# --- HTTP transports: enforce bearer token auth (fail-closed) ---
320323
token = get_bearer_token()
324+
_console = sys.__stderr__ or sys.stderr
321325
if not token:
322-
logger.critical(
323-
"FAIL-CLOSED: %s not set. Refusing to start %s transport "
324-
"without authentication. Set the environment variable or "
325-
"use --transport stdio for local-only access.",
326-
BEARER_TOKEN_ENV,
327-
args.transport,
326+
msg = (
327+
f"FAIL-CLOSED: {BEARER_TOKEN_ENV} not set. Refusing to start "
328+
f"{args.transport} transport without authentication.\n"
329+
f" → Set the environment variable or use --transport stdio "
330+
f"for local-only access.\n"
328331
)
332+
_console.write(msg)
333+
_console.flush()
334+
logger.critical(msg.strip())
329335
sys.exit(1)
330336

331337
token_ok, reason = validate_token_strength(token)
332338
if not token_ok:
333-
logger.critical("FAIL-CLOSED: %s — %s", BEARER_TOKEN_ENV, reason)
339+
msg = f"FAIL-CLOSED: {BEARER_TOKEN_ENV}{reason}\n"
340+
_console.write(msg)
341+
_console.flush()
342+
logger.critical(msg.strip())
334343
sys.exit(1)
335344

336345
if args.transport == "sse":

tests/test_dx_polish.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,16 @@ def test_preflight_catches_missing_bearer_token(self):
105105
assert "OPENSPACE_MCP_BEARER_TOKEN" in token_issues[0].message
106106

107107
def test_preflight_suggestion_is_actionable(self):
108-
"""Suggestions must include a command the user can run."""
108+
"""Suggestions must include a platform-appropriate set command."""
109109
from openspace.deploy.preflight import preflight_check
110110

111111
with patch.dict(os.environ, {}, clear=True):
112112
issues = preflight_check(transport="streamable-http", check_port=False)
113113
token_issues = [i for i in issues if i.check == "bearer-token"]
114114
assert token_issues
115115
suggestion = token_issues[0].suggestion
116-
assert "export" in suggestion or "set" in suggestion.lower()
116+
# Must contain a real shell command, not just descriptive text
117+
assert "$env:" in suggestion or "export " in suggestion
117118

118119
def test_preflight_no_token_check_for_stdio(self):
119120
from openspace.deploy.preflight import check_bearer_token
@@ -201,6 +202,60 @@ def test_preflight_report_all_clear(self):
201202
report = format_preflight_report([])
202203
assert "All checks passed" in report
203204

205+
def test_preflight_bearer_suggestion_platform_aware(self):
206+
"""Suggestion must use platform-appropriate shell syntax."""
207+
from openspace.deploy.preflight import check_bearer_token
208+
209+
with patch.dict(os.environ, {}, clear=True):
210+
result = check_bearer_token("streamable-http")
211+
assert result is not None
212+
if os.name == "nt":
213+
assert "$env:" in result.suggestion
214+
else:
215+
assert "export " in result.suggestion
216+
217+
def test_preflight_skill_store_suggestion_platform_aware(self, tmp_path):
218+
"""Skill store fix suggestion must use platform-appropriate commands."""
219+
from openspace.deploy.preflight import check_skill_store
220+
221+
fake_path = tmp_path / "skills"
222+
fake_path.write_text("not a directory")
223+
result = check_skill_store(str(fake_path))
224+
assert result is not None
225+
if os.name == "nt":
226+
assert "Remove-Item" in result.suggestion
227+
else:
228+
assert "rm " in result.suggestion
229+
230+
231+
# ======================================================================
232+
# Preflight visibility (P0 fix)
233+
# ======================================================================
234+
class TestPreflightVisibility:
235+
"""Preflight output must reach the user, not just the log file."""
236+
237+
def test_preflight_writes_to_console_not_just_logger(self):
238+
"""run_mcp_server must write preflight to sys.__stderr__, not just logger."""
239+
import inspect
240+
from openspace.mcp.server import run_mcp_server
241+
242+
source = inspect.getsource(run_mcp_server)
243+
assert "__stderr__" in source, (
244+
"Preflight output must use sys.__stderr__ to reach console"
245+
)
246+
247+
def test_critical_errors_write_to_console(self):
248+
"""Bearer token errors must also reach console."""
249+
import inspect
250+
from openspace.mcp.server import run_mcp_server
251+
252+
source = inspect.getsource(run_mcp_server)
253+
# Count __stderr__ occurrences — should be used for both preflight AND auth errors
254+
count = source.count("__stderr__")
255+
assert count >= 2, (
256+
f"Expected __stderr__ used for preflight AND auth errors, found {count} uses"
257+
)
258+
204259

205260
# ======================================================================
206261
# Error message UX

0 commit comments

Comments
 (0)