Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ OPENAI_API_KEY=sk-...

# Host to bind the Status Server to
# Default: 0.0.0.0 (all interfaces)
# API_HOST=0.0.0.0
# API_HOST=127.0.0.1 # loopback by default (#935); 0.0.0.0 exposes the API on your network

# Port for the Status Server
# Default: 8080
Expand Down
285 changes: 207 additions & 78 deletions codeframe/cli/app.py

Large diffs are not rendered by default.

102 changes: 90 additions & 12 deletions codeframe/cli/auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

import logging
import os
import sys
from pathlib import Path
from typing import Optional, Tuple

import requests
Expand Down Expand Up @@ -537,7 +539,24 @@ def setup_credential(
None, "--provider", "-p", help="Provider name (e.g., anthropic, github, openai)"
),
value: Optional[str] = typer.Option(
None, "--value", "-v", help="Credential value (API key or token)", hide_input=True
None,
"--value",
"-v",
help=(
"DEPRECATED — the value appears in /proc/<pid>/cmdline and your "
"shell history. Pipe it on stdin or omit this to be prompted."
),
hidden=True,
),
value_stdin: bool = typer.Option(
False,
"--value-stdin",
help="Read the credential value from stdin (for scripts and CI).",
),
value_file: Optional[Path] = typer.Option(
None,
"--value-file",
help="Read the credential value from a file (the first line).",
),
):
"""Configure a credential for a provider.
Expand All @@ -546,16 +565,55 @@ def setup_credential(
and other integrations. Credentials are stored in the system keyring
or an encrypted file.

The value is never taken from the command line by default (#935): anything
in argv is world-readable via /proc/<pid>/cmdline while the process runs and
is written to your shell history.

Examples:

codeframe auth setup # Interactive mode
codeframe auth setup # interactive — prompts, input hidden

codeframe auth setup --provider anthropic # prompts for the value

codeframe auth setup --provider anthropic --value sk-ant-...
echo "$MY_KEY" | codeframe auth setup -p github --value-stdin

codeframe auth setup -p github -v ghp_...
codeframe auth setup -p github --value-file ~/.secrets/gh-token
"""
manager = CredentialManager()

# A non-interactive value source is incompatible with the interactive
# provider prompt: typer.prompt() reads stdin, so with `--value-stdin` and
# no --provider it would consume the piped SECRET as the provider choice and
# then echo it back in "Unknown provider: sk-ant-..." — leaking the exact
# thing this flag exists to protect (PR review on #935).
if (value_stdin or value_file is not None) and not provider:
console.print(
"[red]Error:[/red] --provider is required with --value-stdin/--value-file."
)
console.print(" e.g. echo \"$KEY\" | codeframe auth setup -p github --value-stdin")
raise typer.Exit(1)

if value is not None:
console.print(
"[yellow]WARNING:[/yellow] --value puts the credential in this "
"process's command line, readable by any local process via "
"/proc/<pid>/cmdline, and in your shell history. Use --value-stdin "
"or --value-file instead."
)

# Read the non-interactive value FIRST: stdin can only be consumed once, and
# the provider prompt below also reads it.
if value_stdin:
# .readline() not .read(): a trailing newline from `echo` is not part of
# the secret, and a here-doc may carry more than one line.
value = sys.stdin.readline().strip()
elif value_file is not None:
try:
value = value_file.read_text(encoding="utf-8").splitlines()[0].strip()
except (OSError, IndexError, UnicodeDecodeError) as exc:
console.print(f"[red]Error:[/red] could not read {value_file}: {exc}")
raise typer.Exit(1)

# Interactive provider selection if not provided
if not provider:
console.print("\n[bold]Select a provider to configure:[/bold]\n")
Expand All @@ -577,8 +635,13 @@ def setup_credential(
# Resolve provider name
try:
provider_enum = resolve_provider_name(provider)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
except ValueError:
# Deliberately does NOT echo the rejected value: if stdin were ever
# mis-consumed the value would be the credential itself.
console.print(
"[red]Error:[/red] Unknown provider. Expected one of: "
"anthropic, openai, github, gitlab."
)
raise typer.Exit(1)

# Prompt for value if not provided
Expand Down Expand Up @@ -675,8 +738,13 @@ def validate_credential(
# Resolve provider
try:
provider_enum = resolve_provider_name(provider)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
except ValueError:
# Deliberately does NOT echo the rejected value: if stdin were ever
# mis-consumed the value would be the credential itself.
console.print(
"[red]Error:[/red] Unknown provider. Expected one of: "
"anthropic, openai, github, gitlab."
)
raise typer.Exit(1)

# Get credential
Expand Down Expand Up @@ -733,8 +801,13 @@ def rotate_credential(
# Resolve provider
try:
provider_enum = resolve_provider_name(provider)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
except ValueError:
# Deliberately does NOT echo the rejected value: if stdin were ever
# mis-consumed the value would be the credential itself.
console.print(
"[red]Error:[/red] Unknown provider. Expected one of: "
"anthropic, openai, github, gitlab."
)
raise typer.Exit(1)

# Check if credential exists
Expand Down Expand Up @@ -800,8 +873,13 @@ def remove_credential(
# Resolve provider
try:
provider_enum = resolve_provider_name(provider)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
except ValueError:
# Deliberately does NOT echo the rejected value: if stdin were ever
# mis-consumed the value would be the credential itself.
console.print(
"[red]Error:[/red] Unknown provider. Expected one of: "
"anthropic, openai, github, gitlab."
)
raise typer.Exit(1)

# Check if credential exists in storage (not just environment)
Expand Down
4 changes: 3 additions & 1 deletion codeframe/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,9 @@ class GlobalConfig(BaseSettings):
database_path: str = Field(".codeframe/state.db", alias="DATABASE_PATH")

# Status Server configuration
api_host: str = Field("0.0.0.0", alias="API_HOST")
# Loopback by default (#935): the API exposes SQLite state, workspace
# file access and agent execution. Set API_HOST=0.0.0.0 to expose it.
api_host: str = Field("127.0.0.1", alias="API_HOST")
api_port: int = Field(8080, alias="API_PORT")
cors_origins: str = Field("http://localhost:3000,http://localhost:5173", alias="CORS_ORIGINS")

Expand Down
10 changes: 6 additions & 4 deletions codeframe/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from typing import Optional

from rich.markup import escape

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
Expand Down Expand Up @@ -219,7 +221,7 @@ def _update_task_table(self, data: DashboardData) -> None:
color = _STATUS_COLORS.get(status_val, "white")
table.add_row(
task.id[:8],
task.title[:50],
escape(task.title[:50]),
f"[{color}]{status_val}[/{color}]",
str(task.priority),
)
Expand All @@ -241,7 +243,7 @@ def _update_blocker_panel(self, data: DashboardData) -> None:
return

for blocker in data.blockers:
log.write(f"[red bold]{blocker.id[:8]}[/red bold]: {blocker.question[:60]}")
log.write(f"[red bold]{blocker.id[:8]}[/red bold]: {escape(blocker.question[:60])}")
Comment thread
frankbria marked this conversation as resolved.

_SEVERITY_COLORS: dict[str, str] = {
"critical": "red bold",
Expand Down Expand Up @@ -269,7 +271,7 @@ def _update_proof_panel(self, data: DashboardData) -> None:
from datetime import date
days = (req.waiver.expires - date.today()).days
log.write(
f"[yellow bold]⚠ {req.id}[/yellow bold]: waiver expires in {days}d — {req.title[:50]}"
f"[yellow bold]⚠ {req.id}[/yellow bold]: waiver expires in {days}d — {escape(req.title[:50])}"
)

for req in data.open_requirements:
Expand All @@ -281,7 +283,7 @@ def _update_proof_panel(self, data: DashboardData) -> None:
sev_color = self._SEVERITY_COLORS.get(req.severity.value, "white")
log.write(
f"[red]{req.id}[/red] [{sev_color}]{req.severity.value}[/{sev_color}]"
f" {req.title[:45]} | {gate_summary}"
f" {escape(req.title[:45])} | {gate_summary}"
)

def action_refresh(self) -> None:
Expand Down
6 changes: 4 additions & 2 deletions codeframe/ui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ async def test_broadcast(message: dict, project_id: int = None):
# ============================================================================


def run_server(host: str = "0.0.0.0", port: int = 8080):
def run_server(host: str = "127.0.0.1", port: int = 8080):
"""Run the Status Server."""
import uvicorn

Expand All @@ -908,7 +908,9 @@ def run_server(host: str = "0.0.0.0", port: int = 8080):
parser.add_argument(
"--host",
type=str,
default=os.environ.get("HOST", "0.0.0.0"),
# Loopback by default (#935): this process exposes SQLite state,
# workspace file access and agent execution. Set HOST=0.0.0.0 to expose it.
default=os.environ.get("HOST", "127.0.0.1"),
help="Host to bind to (default: 0.0.0.0 or HOST env var)",
)
parser.add_argument(
Expand Down
Loading
Loading