Persist gateway secrets and worktree state in user data

This commit is contained in:
HumbleTurtle
2026-09-21 13:19:27 -03:00
parent 19b047e7f8
commit 76f5b18b8a
5 changed files with 531 additions and 41 deletions
+38 -40
View File
@@ -49,6 +49,8 @@ import webbrowser
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from napseer_credentials import WorkspacePaths, credential_lock
from napseer_spake2 import (
relay_aad,
relay_context_hash,
@@ -71,31 +73,17 @@ class RelaySocketClosed(RuntimeError):
def local_state_dir():
preferred = pathlib.Path.cwd() / ".napseer"
legacy = pathlib.Path.cwd() / "napseer"
if preferred.exists() or not legacy.exists():
return preferred
return legacy
return WorkspacePaths().state
def state_dir_status_payload():
preferred = pathlib.Path.cwd() / ".napseer"
legacy = pathlib.Path.cwd() / "napseer"
active = AUTH_DIR
if preferred.exists() and legacy.exists():
message = "Both .napseer and legacy napseer directories exist; .napseer is active."
elif active == legacy:
message = "Legacy napseer directory is active because .napseer does not exist."
else:
message = ".napseer directory is active."
return {
"active_state_dir": str(active),
"preferred_state_dir": str(preferred),
"legacy_state_dir": str(legacy),
"legacy_state_dir_active": active == legacy,
"preferred_state_dir_exists": preferred.exists(),
"legacy_state_dir_exists": legacy.exists(),
"state_dir_message": message,
"active_state_dir": str(AUTH_DIR),
"state_dir": str(AUTH_DIR),
"state_dir_exists": AUTH_DIR.exists(),
"project_root": str(WORKSPACE.root),
"state_dir_message": "Private gateway state is stored in user data.",
"migration_required": bool(WORKSPACE.migration_sources()),
}
@@ -206,7 +194,11 @@ def send_telemetry_event_async(event, component, outcome="success", **fields):
).start()
AUTH_DIR = local_state_dir()
WORKSPACE = WorkspacePaths()
# The pre-dot-directory gateway layout is migration input only.
if not WORKSPACE.legacy.exists() and any((WORKSPACE.root / "napseer" / name).is_file() for name in ("auth.json", "vault.json")):
WORKSPACE.legacy = WORKSPACE.root / "napseer"
AUTH_DIR = WORKSPACE.state
AUTH_PATH = AUTH_DIR / "auth.json"
VAULT_PATH = AUTH_DIR / "vault.json"
GATEWAY_RELAY_SECRET_PATH = AUTH_DIR / "gateway-relay-secret.json"
@@ -216,10 +208,10 @@ CONTAINER_IDENTITY_PATH = AUTH_DIR / "container-identity.json"
TELEMETRY_PATH = AUTH_DIR / "telemetry.json"
INDEX_PATH = AUTH_DIR / "index.sqlite"
INDEX_LOCK_PATH = AUTH_DIR / "index.lock"
DEFAULT_KEY_PATH = AUTH_DIR / "id_ed25519"
DEFAULT_KEY_PATH = WORKSPACE.signing_key
SCRIPT_PATH = pathlib.Path(__file__).resolve()
SCRIPT_NAME = SCRIPT_PATH.name
SCRIPT_DEPENDENCY_NAMES = ("napseer_spake2.py",)
SCRIPT_DEPENDENCY_NAMES = ("napseer_spake2.py", "napseer_credentials.py")
CONTRACT_VERSION = "napseer.mcp.contract.v1"
TELEMETRY_DISABLED_VALUES = {"0", "false", "no", "off", "disabled"}
TELEMETRY_ALLOWED_FIELDS = {
@@ -687,7 +679,7 @@ def write_vault(passphrase, payload):
key = _derive_vault_key(passphrase, salt, VAULT_KDF_ITERATIONS)
plaintext = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
nonce, ciphertext, tag = _encrypt_vault_payload(key, plaintext, VAULT_SCHEMA.encode("utf-8"))
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
VAULT_PATH.write_text(
json.dumps(
@@ -746,7 +738,7 @@ def derive_gateway_relay_secret(passphrase):
def write_gateway_relay_secret(passphrase):
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
GATEWAY_RELAY_SECRET_PATH.write_text(
json.dumps(
@@ -802,7 +794,7 @@ def write_gateway_relay_state(**updates):
current.update({key: value for key, value in updates.items() if value is not None})
current["schema"] = GATEWAY_RELAY_STATE_SCHEMA
current["updated_at"] = iso_now()
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
GATEWAY_RELAY_STATE_PATH.write_text(json.dumps(current, indent=2) + "\n", encoding="utf-8")
chmod_best_effort(GATEWAY_RELAY_STATE_PATH, 0o600)
@@ -931,7 +923,7 @@ def ensure_container_uuid():
"container_uuid": value,
"created_at": iso_now(),
}
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
CONTAINER_IDENTITY_PATH.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
chmod_best_effort(CONTAINER_IDENTITY_PATH, 0o600)
@@ -1164,7 +1156,7 @@ def clean_terminal_name(value, fallback="terminal"):
def default_gateway_session_name():
source = AUTH.get("project_slug") or AUTH.get("project_name") or pathlib.Path.cwd().name or "project"
return f"napseer-{clean_tmux_name(source, 'project')}"
return f"napseer-{clean_tmux_name(source, 'project')}-{WORKSPACE.workspace_id[:10]}"
def normalize_gateway_command(value=None):
@@ -1727,7 +1719,7 @@ def write_gateway_schedules_state(state):
state["schema"] = GATEWAY_SCHEDULES_SCHEMA
state["version"] = 1
state["updated_at"] = iso_now()
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
GATEWAY_SCHEDULES_PATH.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8")
chmod_best_effort(GATEWAY_SCHEDULES_PATH, 0o600)
@@ -5135,7 +5127,7 @@ def request_project_write(method, path, payload, project_id, purpose, scope_type
def index_file_lock():
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
handle = INDEX_LOCK_PATH.open("a+b")
chmod_best_effort(INDEX_LOCK_PATH, 0o600)
@@ -5153,7 +5145,7 @@ def release_index_file_lock(handle):
def index_connect():
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
conn = sqlite3.connect(str(INDEX_PATH), timeout=30)
conn.row_factory = sqlite3.Row
@@ -6032,20 +6024,21 @@ def save_auth(updates):
def ensure_local_files():
AUTH_DIR.mkdir(exist_ok=True)
AUTH_DIR.mkdir(parents=True, exist_ok=True)
chmod_best_effort(AUTH_DIR, 0o700)
gitignore = AUTH_DIR / ".gitignore"
if not gitignore.exists():
gitignore.write_text("*\n!.gitignore\n", encoding="utf-8")
key_path = pathlib.Path(AUTH["ssh_key_path"] or DEFAULT_KEY_PATH).expanduser()
public_key_path = pathlib.Path(str(key_path) + ".pub")
if not key_path.exists():
key_path.parent.mkdir(exist_ok=True)
chmod_best_effort(key_path.parent, 0o700)
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key_path)],
check=True,
)
with credential_lock(key_path):
if not key_path.exists():
key_path.parent.mkdir(parents=True, exist_ok=True)
chmod_best_effort(key_path.parent, 0o700)
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key_path)],
check=True,
)
chmod_best_effort(key_path, 0o600)
if public_key_path.exists():
chmod_best_effort(public_key_path, 0o644)
@@ -9816,6 +9809,11 @@ def handle(message):
def cli_main(argv):
command = argv[1] if len(argv) > 1 else ""
if argv[1:] == ["auth", "migrate"]:
print(json.dumps(WORKSPACE.migrate(gateway=True), indent=2))
return
if command not in {"help", "-h", "--help"}:
WORKSPACE.require_migrated()
if command in {"configure", "config"}:
print(json.dumps(cli_configure_status(), indent=2))
return