mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-02 02:42:04 +00:00
Add agent security controls, broker auth, and capability discovery
This commit is contained in:
1 parent
1febbd4b2c
commit
fe96ccd7c7
15 files changed
+2381
-99
No files matched your search
@@ -39,6 +39,7 @@ deterministic_totp_option = typer.Option(
|
||||
|
||||
# Sub command groups
|
||||
from . import entry, vault, nostr, config, fingerprint, util, api, agent
|
||||
from .capabilities import register_capabilities_command
|
||||
|
||||
app.add_typer(entry.app, name="entry")
|
||||
app.add_typer(vault.app, name="vault")
|
||||
@@ -48,6 +49,7 @@ app.add_typer(fingerprint.app, name="fingerprint")
|
||||
app.add_typer(util.app, name="util")
|
||||
app.add_typer(api.app, name="api")
|
||||
app.add_typer(agent.app, name="agent")
|
||||
register_capabilities_command(app)
|
||||
|
||||
|
||||
def run() -> None:
|
||||
|
||||
+1056
-88
File diff suppressed because it is too large.
Load diff
+53
-2
@@ -1,17 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import typer
|
||||
import uvicorn
|
||||
|
||||
from .. import api as api_module
|
||||
from seedpass.core.auth_broker import AuthBrokerError, resolve_password as resolve_broker_password
|
||||
|
||||
app = typer.Typer(help="Run the API server")
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def api_start(ctx: typer.Context, host: str = "127.0.0.1", port: int = 8000) -> None:
|
||||
def api_start(
|
||||
ctx: typer.Context,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
unlock: bool = typer.Option(
|
||||
False, "--unlock", help="Unlock vault during startup using the selected auth broker"
|
||||
),
|
||||
auth_broker: str = typer.Option(
|
||||
"prompt",
|
||||
"--auth-broker",
|
||||
help="Password source when --unlock is used (prompt|env|keyring|command)",
|
||||
click_type=click.Choice(["prompt", "env", "keyring", "command"], case_sensitive=False),
|
||||
),
|
||||
password_env: str = typer.Option(
|
||||
"SEEDPASS_PASSWORD", "--password-env", help="Env var used when --auth-broker=env"
|
||||
),
|
||||
broker_service: str = typer.Option(
|
||||
"seedpass",
|
||||
"--broker-service",
|
||||
help="Keyring service name when --auth-broker=keyring",
|
||||
),
|
||||
broker_account: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--broker-account",
|
||||
help="Keyring account (defaults to active fingerprint when available)",
|
||||
),
|
||||
broker_command: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--broker-command",
|
||||
help="Command that prints password to stdout for --auth-broker=command",
|
||||
),
|
||||
) -> None:
|
||||
"""Start the SeedPass API server."""
|
||||
token = api_module.start_server(ctx.obj.get("fingerprint"))
|
||||
unlock_password: str | None = None
|
||||
if unlock:
|
||||
if auth_broker.lower() == "prompt":
|
||||
unlock_password = typer.prompt("Master password", hide_input=True)
|
||||
else:
|
||||
account = broker_account or str((ctx.obj or {}).get("fingerprint") or "default")
|
||||
try:
|
||||
unlock_password = resolve_broker_password(
|
||||
broker=auth_broker,
|
||||
password_env=password_env,
|
||||
broker_service=broker_service,
|
||||
broker_account=account,
|
||||
broker_command=broker_command,
|
||||
)
|
||||
except AuthBrokerError as exc:
|
||||
raise typer.BadParameter(str(exc)) from exc
|
||||
token = api_module.start_server(ctx.obj.get("fingerprint"), unlock_password)
|
||||
typer.echo(
|
||||
f"API token: {token}\nWARNING: Store this token securely; it cannot be recovered."
|
||||
)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import typer
|
||||
|
||||
|
||||
def build_capabilities() -> dict[str, Any]:
|
||||
"""Return a deterministic capability map for CLI/API discovery."""
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"product": "SeedPass",
|
||||
"interfaces": {
|
||||
"cli": {
|
||||
"root_commands": [
|
||||
"lock",
|
||||
"gui",
|
||||
"capabilities",
|
||||
"entry",
|
||||
"vault",
|
||||
"nostr",
|
||||
"config",
|
||||
"fingerprint",
|
||||
"util",
|
||||
"api",
|
||||
"agent",
|
||||
],
|
||||
"global_options": [
|
||||
"--fingerprint",
|
||||
"--no-clipboard",
|
||||
"--deterministic-totp",
|
||||
],
|
||||
},
|
||||
"api": {
|
||||
"discovery": ["/docs"],
|
||||
"auth_headers": ["Authorization: Bearer <token>"],
|
||||
"sensitive_auth_headers": ["X-SeedPass-Password"],
|
||||
},
|
||||
},
|
||||
"security_features": {
|
||||
"auth_brokers": {
|
||||
"available": ["env", "keyring", "command", "prompt"],
|
||||
"agent_commands": ["agent init", "agent get"],
|
||||
"vault_commands": ["vault unlock"],
|
||||
"api_commands": ["api start --unlock"],
|
||||
},
|
||||
"policy": {
|
||||
"commands": ["agent policy-show", "agent policy-lint", "agent policy-set"],
|
||||
"default_effect": "deny",
|
||||
},
|
||||
"tokens": {
|
||||
"commands": ["agent token-issue", "agent token-list", "agent token-revoke"],
|
||||
"supports_ttl": True,
|
||||
"supports_use_limits": True,
|
||||
"supports_revocation": True,
|
||||
},
|
||||
"redaction": {
|
||||
"default_safe_output": True,
|
||||
"reveal_override": "agent get --reveal",
|
||||
},
|
||||
"audit": {
|
||||
"log_file": "~/.seedpass/agent_audit.log",
|
||||
"integrity": "HMAC chained append-only records",
|
||||
},
|
||||
},
|
||||
"high_risk_operations": [
|
||||
"vault export",
|
||||
"vault reveal-parent-seed",
|
||||
"entry get (sensitive kinds)",
|
||||
"api /api/v1/vault/export",
|
||||
],
|
||||
"help_hints": [
|
||||
"Run `seedpass --help` for top-level commands.",
|
||||
"Run `seedpass <group> --help` for detailed options.",
|
||||
"Use `seedpass capabilities --format json` for machine-readable discovery.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def emit_capabilities(output_format: str) -> str:
|
||||
data = build_capabilities()
|
||||
if output_format == "json":
|
||||
return json.dumps(data, indent=2, sort_keys=True)
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append("SeedPass Capabilities (schema v1)")
|
||||
lines.append("")
|
||||
lines.append("CLI root commands:")
|
||||
lines.append(", ".join(data["interfaces"]["cli"]["root_commands"]))
|
||||
lines.append("")
|
||||
lines.append("Security highlights:")
|
||||
lines.append("- Auth brokers: env, keyring, command, prompt")
|
||||
lines.append("- Policy controls: agent policy-show/policy-lint/policy-set")
|
||||
lines.append("- Scoped tokens: issue/list/revoke with TTL and use limits")
|
||||
lines.append("- Safe output redaction default with explicit --reveal")
|
||||
lines.append("- Audited access log with chained HMAC signatures")
|
||||
lines.append("")
|
||||
lines.append("Discovery hints:")
|
||||
for hint in data["help_hints"]:
|
||||
lines.append(f"- {hint}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def register_capabilities_command(app: typer.Typer) -> None:
|
||||
@app.command("capabilities")
|
||||
def capabilities_command(
|
||||
format: str = typer.Option(
|
||||
"text",
|
||||
"--format",
|
||||
help="Output format for capabilities map",
|
||||
click_type=click.Choice(["text", "json"], case_sensitive=False),
|
||||
)
|
||||
) -> None:
|
||||
"""Show a deterministic capabilities map for users and agents."""
|
||||
typer.echo(emit_capabilities(format.lower()))
|
||||
@@ -9,6 +9,7 @@ import typer
|
||||
import click
|
||||
|
||||
from .common import _get_entry_service, EntryType
|
||||
from seedpass.core.agent_export_policy import kind_export_allowed, load_export_policy
|
||||
from seedpass.core.entry_types import ALL_ENTRY_TYPES
|
||||
from utils.clipboard import ClipboardUnavailableError
|
||||
|
||||
@@ -357,9 +358,20 @@ def entry_totp_codes(ctx: typer.Context) -> None:
|
||||
|
||||
@app.command("export-totp")
|
||||
def entry_export_totp(
|
||||
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
|
||||
ctx: typer.Context,
|
||||
file: str = typer.Option(..., help="Output file"),
|
||||
agent_profile: bool = typer.Option(
|
||||
False,
|
||||
"--agent-profile",
|
||||
help="Apply agent export policy controls",
|
||||
),
|
||||
) -> None:
|
||||
"""Export all TOTP secrets to a JSON file."""
|
||||
if agent_profile:
|
||||
policy = load_export_policy()
|
||||
if not kind_export_allowed(policy, EntryType.TOTP.value):
|
||||
typer.echo("Error: Policy denied TOTP export for agent profile.")
|
||||
raise typer.Exit(code=1)
|
||||
service = _get_entry_service(ctx)
|
||||
data = service.export_totp_entries()
|
||||
Path(file).write_text(json.dumps(data, indent=2))
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import typer
|
||||
|
||||
from .common import (
|
||||
@@ -12,16 +13,57 @@ from .common import (
|
||||
UnlockRequest,
|
||||
BackupParentSeedRequest,
|
||||
)
|
||||
from seedpass.core.agent_export_policy import (
|
||||
allowed_kinds,
|
||||
filter_index_for_allowed_kinds,
|
||||
full_export_allowed,
|
||||
load_export_policy,
|
||||
)
|
||||
from seedpass.core.auth_broker import AuthBrokerError, resolve_password as resolve_broker_password
|
||||
|
||||
app = typer.Typer(help="Manage the entire vault")
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def vault_export(
|
||||
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
|
||||
ctx: typer.Context,
|
||||
file: str = typer.Option(..., help="Output file"),
|
||||
agent_profile: bool = typer.Option(
|
||||
False,
|
||||
"--agent-profile",
|
||||
help="Apply agent export policy controls",
|
||||
),
|
||||
policy_filtered: bool = typer.Option(
|
||||
False,
|
||||
"--policy-filtered",
|
||||
help="Export only policy-allowed subset of entries",
|
||||
),
|
||||
) -> None:
|
||||
"""Export the vault profile to an encrypted file."""
|
||||
vault_service, _profile, _sync = _get_services(ctx)
|
||||
if agent_profile:
|
||||
policy = load_export_policy()
|
||||
if policy_filtered:
|
||||
manager = getattr(vault_service, "_manager", None)
|
||||
vault = getattr(manager, "vault", None)
|
||||
enc_mgr = getattr(vault, "encryption_manager", None)
|
||||
if vault is None or enc_mgr is None:
|
||||
typer.echo("Error: policy-filtered export unavailable in current context")
|
||||
raise typer.Exit(code=1)
|
||||
index_data = vault.load_index()
|
||||
filtered = filter_index_for_allowed_kinds(index_data, allowed_kinds(policy))
|
||||
payload = json.dumps(
|
||||
filtered, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
data = enc_mgr.encrypt_data(payload)
|
||||
Path(file).write_bytes(data)
|
||||
typer.echo(str(file))
|
||||
return
|
||||
if not full_export_allowed(policy):
|
||||
typer.echo(
|
||||
"Error: Policy denied full vault export for agent profile. Use --policy-filtered."
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
data = vault_service.export_profile()
|
||||
Path(file).write_bytes(data)
|
||||
typer.echo(str(file))
|
||||
@@ -55,16 +97,59 @@ def vault_change_password(ctx: typer.Context) -> None:
|
||||
|
||||
|
||||
@app.command("unlock")
|
||||
def vault_unlock(ctx: typer.Context) -> None:
|
||||
def vault_unlock(
|
||||
ctx: typer.Context,
|
||||
auth_broker: str = typer.Option(
|
||||
"prompt",
|
||||
"--auth-broker",
|
||||
help="Password source for unlock (prompt|env|keyring|command)",
|
||||
click_type=click.Choice(["prompt", "env", "keyring", "command"], case_sensitive=False),
|
||||
),
|
||||
password_env: str = typer.Option(
|
||||
"SEEDPASS_PASSWORD", "--password-env", help="Env var used when --auth-broker=env"
|
||||
),
|
||||
broker_service: str = typer.Option(
|
||||
"seedpass",
|
||||
"--broker-service",
|
||||
help="Keyring service name when --auth-broker=keyring",
|
||||
),
|
||||
broker_account: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--broker-account",
|
||||
help="Keyring account (defaults to active fingerprint when available)",
|
||||
),
|
||||
broker_command: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--broker-command",
|
||||
help="Command that prints password to stdout for --auth-broker=command",
|
||||
),
|
||||
) -> None:
|
||||
"""Unlock the vault for the active profile."""
|
||||
vault_service, _profile, _sync = _get_services(ctx)
|
||||
password = typer.prompt("Master password", hide_input=True)
|
||||
password: str
|
||||
if auth_broker.lower() == "prompt":
|
||||
password = typer.prompt("Master password", hide_input=True)
|
||||
else:
|
||||
account = broker_account or str((ctx.obj or {}).get("fingerprint") or "default")
|
||||
try:
|
||||
password = resolve_broker_password(
|
||||
broker=auth_broker,
|
||||
password_env=password_env,
|
||||
broker_service=broker_service,
|
||||
broker_account=account,
|
||||
broker_command=broker_command,
|
||||
)
|
||||
except AuthBrokerError as exc:
|
||||
raise typer.BadParameter(str(exc)) from exc
|
||||
try:
|
||||
resp = vault_service.unlock(UnlockRequest(password=password))
|
||||
except Exception as exc: # pragma: no cover - pass through errors
|
||||
typer.echo(f"Error: {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
typer.echo(f"Unlocked in {resp.duration:.2f}s")
|
||||
typer.echo(
|
||||
"Tip: run `seedpass --help` (and `seedpass <command> --help`) to discover available features."
|
||||
)
|
||||
|
||||
|
||||
@app.command("lock")
|
||||
|
||||
Reference in new issue
Block a user