Remove local gateway unlock gate
This commit is contained in:
@@ -591,18 +591,18 @@ def handle_gateway(args):
|
|||||||
subcommand = args[0] if args else "ls"
|
subcommand = args[0] if args else "ls"
|
||||||
rest = args[1:] if args else []
|
rest = args[1:] if args else []
|
||||||
if subcommand in {"help", "-h", "--help"}:
|
if subcommand in {"help", "-h", "--help"}:
|
||||||
print("Usage: nap gateway [start|stop|status|logs|configure|setup|unlock|lock|restart|kill|vault|terminal|schedule|service]")
|
print("Usage: nap gateway [start|stop|status|logs|configure|setup|restart|kill|vault|terminal|schedule|service]")
|
||||||
print(" nap gateway start [--port PORT] Start the local gateway service.")
|
print(" nap gateway start [--port PORT] Start the local gateway service.")
|
||||||
print(" nap gateway status Show the managed gateway process.")
|
print(" nap gateway status Show the managed gateway process.")
|
||||||
print(" nap gateway configure [--command CMD] Configure the local gateway command.")
|
print(" nap gateway configure [--command CMD] Configure the local gateway command.")
|
||||||
print(" nap gateway setup Create or unlock the encrypted gateway vault.")
|
print(" nap gateway setup Create or migrate local gateway storage.")
|
||||||
print(" nap gateway vault Show gateway vault status and pending setup requests.")
|
print(" nap gateway vault Show gateway vault status and pending setup requests.")
|
||||||
print(" nap gateway vault process Complete pending setup requests.")
|
print(" nap gateway vault process Complete pending setup requests.")
|
||||||
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway|memory")
|
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway|memory")
|
||||||
return 0
|
return 0
|
||||||
if subcommand == "vault" and rest and rest[0] in {"help", "-h", "--help"}:
|
if subcommand == "vault" and rest and rest[0] in {"help", "-h", "--help"}:
|
||||||
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all] [--project-id ID]")
|
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all] [--project-id ID]")
|
||||||
print(" status Show configured/locked state and pending setup requests.")
|
print(" status Show local gateway state and pending setup requests.")
|
||||||
print(" process Upload opaque client-wrapped key bundle records for backend-owned HashiCorp storage.")
|
print(" process Upload opaque client-wrapped key bundle records for backend-owned HashiCorp storage.")
|
||||||
print(" rotate-secret Rotate one encrypted project secret.")
|
print(" rotate-secret Rotate one encrypted project secret.")
|
||||||
return 0
|
return 0
|
||||||
@@ -616,9 +616,9 @@ def handle_gateway(args):
|
|||||||
return print_json(service_logs("gateway", rest)) or 0
|
return print_json(service_logs("gateway", rest)) or 0
|
||||||
if subcommand == "configure":
|
if subcommand == "configure":
|
||||||
return run_wrapper("gateway", ["configure", *rest])
|
return run_wrapper("gateway", ["configure", *rest])
|
||||||
if subcommand in {"setup", "unlock", "lock", "restart", "kill", "vault", "terminal", "schedule", "service"}:
|
if subcommand in {"setup", "restart", "kill", "vault", "terminal", "schedule", "service"}:
|
||||||
return run_wrapper("gateway", [subcommand, *rest])
|
return run_wrapper("gateway", [subcommand, *rest])
|
||||||
raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|unlock|lock|restart|kill|logs|status|vault|terminal|schedule|service")
|
raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|restart|kill|logs|status|vault|terminal|schedule|service")
|
||||||
|
|
||||||
|
|
||||||
def print_json(value):
|
def print_json(value):
|
||||||
@@ -675,8 +675,8 @@ def main(argv):
|
|||||||
print(" Example: nap auth login-local")
|
print(" Example: nap auth login-local")
|
||||||
print(" project Project commands: create, status, encryption.")
|
print(" project Project commands: create, status, encryption.")
|
||||||
print(" Examples: nap project create, nap project status, nap project encryption status")
|
print(" Examples: nap project create, nap project status, nap project encryption status")
|
||||||
print(" Encrypted project setup is completed by an unlocked gateway: nap gateway vault process")
|
print(" Encrypted project setup is completed by the local gateway: nap gateway vault process")
|
||||||
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, restart, kill, logs, status, vault.")
|
print(" gateway Gateway commands: start, stop, configure, setup, restart, kill, logs, status, vault.")
|
||||||
print(" Start auto-selects a local port; use --port to pin one.")
|
print(" Start auto-selects a local port; use --port to pin one.")
|
||||||
print(" Examples: nap gateway start, nap gateway vault, nap gateway vault rotate-secret --kind chat")
|
print(" Examples: nap gateway start, nap gateway vault, nap gateway vault rotate-secret --kind chat")
|
||||||
print(" agent Agent commands: list, workspaces, create, show, edit.")
|
print(" agent Agent commands: list, workspaces, create, show, edit.")
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ def send_telemetry_event_async(event, component, outcome="success", **fields):
|
|||||||
AUTH_DIR = local_state_dir()
|
AUTH_DIR = local_state_dir()
|
||||||
AUTH_PATH = AUTH_DIR / "auth.json"
|
AUTH_PATH = AUTH_DIR / "auth.json"
|
||||||
VAULT_PATH = AUTH_DIR / "vault.json"
|
VAULT_PATH = AUTH_DIR / "vault.json"
|
||||||
|
LOCAL_VAULT_KEY_PATH = AUTH_DIR / "vault.local-key"
|
||||||
GATEWAY_RELAY_SECRET_PATH = AUTH_DIR / "gateway-relay-secret.json"
|
GATEWAY_RELAY_SECRET_PATH = AUTH_DIR / "gateway-relay-secret.json"
|
||||||
GATEWAY_RELAY_STATE_PATH = AUTH_DIR / "gateway-relay-state.json"
|
GATEWAY_RELAY_STATE_PATH = AUTH_DIR / "gateway-relay-state.json"
|
||||||
GATEWAY_SCHEDULES_PATH = AUTH_DIR / "gateway-schedules.json"
|
GATEWAY_SCHEDULES_PATH = AUTH_DIR / "gateway-schedules.json"
|
||||||
@@ -440,8 +441,6 @@ PUBLIC_TOOLS = {
|
|||||||
"nap_whoami",
|
"nap_whoami",
|
||||||
"nap_gateway_status",
|
"nap_gateway_status",
|
||||||
"nap_gateway_setup",
|
"nap_gateway_setup",
|
||||||
"nap_gateway_unlock",
|
|
||||||
"nap_gateway_lock",
|
|
||||||
"nap_gateway_configure",
|
"nap_gateway_configure",
|
||||||
"nap_gateway_vault_ls",
|
"nap_gateway_vault_ls",
|
||||||
"nap_gateway_vault_process",
|
"nap_gateway_vault_process",
|
||||||
@@ -696,13 +695,13 @@ def _decrypt_vault_payload(key, nonce, ciphertext, tag, aad=b""):
|
|||||||
one_time_key = _chacha20_block(key, 0, nonce)[:32]
|
one_time_key = _chacha20_block(key, 0, nonce)[:32]
|
||||||
expected = _poly1305_mac(_aead_mac_data(aad, ciphertext), one_time_key)
|
expected = _poly1305_mac(_aead_mac_data(aad, ciphertext), one_time_key)
|
||||||
if not hmac.compare_digest(expected, tag):
|
if not hmac.compare_digest(expected, tag):
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
return _chacha20_xor(key, nonce, ciphertext)
|
return _chacha20_xor(key, nonce, ciphertext)
|
||||||
|
|
||||||
|
|
||||||
def _derive_vault_key(passphrase, salt, iterations):
|
def _derive_vault_key(passphrase, salt, iterations):
|
||||||
if not isinstance(passphrase, str) or not passphrase:
|
if not isinstance(passphrase, str) or not passphrase:
|
||||||
raise ValueError("master passphrase is required")
|
raise ValueError("local gateway vault key is required")
|
||||||
return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, iterations, dklen=32)
|
return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, iterations, dklen=32)
|
||||||
|
|
||||||
|
|
||||||
@@ -712,14 +711,32 @@ def read_master_passphrase(create=False):
|
|||||||
return value
|
return value
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
raise RuntimeError("NAPSEER_MASTER_PASSPHRASE is required in non-interactive mode")
|
raise RuntimeError("NAPSEER_MASTER_PASSPHRASE is required in non-interactive mode")
|
||||||
first = getpass.getpass("Create master passphrase: " if create else "Master passphrase: ")
|
first = getpass.getpass("Create gateway relay passphrase: " if create else "Gateway relay passphrase: ")
|
||||||
if create:
|
if create:
|
||||||
second = getpass.getpass("Confirm master passphrase: ")
|
second = getpass.getpass("Confirm gateway relay passphrase: ")
|
||||||
if first != second:
|
if first != second:
|
||||||
raise RuntimeError("master passphrases did not match")
|
raise RuntimeError("gateway relay passphrases did not match")
|
||||||
return first
|
return first
|
||||||
|
|
||||||
|
|
||||||
|
def read_local_vault_key(create=False):
|
||||||
|
value = os.environ.get("NAPSEER_LOCAL_VAULT_KEY")
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
if LOCAL_VAULT_KEY_PATH.exists():
|
||||||
|
value = LOCAL_VAULT_KEY_PATH.read_text(encoding="utf-8").strip()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
if not create:
|
||||||
|
return None
|
||||||
|
AUTH_DIR.mkdir(exist_ok=True)
|
||||||
|
chmod_best_effort(AUTH_DIR, 0o700)
|
||||||
|
value = base64.b64encode(secrets.token_bytes(32)).decode("ascii")
|
||||||
|
LOCAL_VAULT_KEY_PATH.write_text(value + "\n", encoding="utf-8")
|
||||||
|
chmod_best_effort(LOCAL_VAULT_KEY_PATH, 0o600)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def vault_exists():
|
def vault_exists():
|
||||||
return VAULT_PATH.exists()
|
return VAULT_PATH.exists()
|
||||||
|
|
||||||
@@ -750,13 +767,13 @@ def write_public_auth(data):
|
|||||||
def read_vault(passphrase, return_key=False):
|
def read_vault(passphrase, return_key=False):
|
||||||
envelope = json.loads(VAULT_PATH.read_text(encoding="utf-8"))
|
envelope = json.loads(VAULT_PATH.read_text(encoding="utf-8"))
|
||||||
if envelope.get("schema") != VAULT_SCHEMA:
|
if envelope.get("schema") != VAULT_SCHEMA:
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
if envelope.get("format") == "plaintext-local" or "payload" in envelope:
|
if envelope.get("format") == "plaintext-local" or "payload" in envelope:
|
||||||
raise ValueError("plaintext local gateway vault is unsupported; rerun gateway setup with a passphrase")
|
raise ValueError("plaintext local gateway vault is unsupported; rerun gateway setup with a passphrase")
|
||||||
kdf = envelope.get("kdf") or {}
|
kdf = envelope.get("kdf") or {}
|
||||||
cipher = envelope.get("cipher") or {}
|
cipher = envelope.get("cipher") or {}
|
||||||
if kdf.get("name") != "pbkdf2-sha256" or cipher.get("name") != "chacha20-poly1305":
|
if kdf.get("name") != "pbkdf2-sha256" or cipher.get("name") != "chacha20-poly1305":
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
salt = base64.b64decode(kdf["salt"])
|
salt = base64.b64decode(kdf["salt"])
|
||||||
iterations = int(kdf["iterations"])
|
iterations = int(kdf["iterations"])
|
||||||
nonce = base64.b64decode(cipher["nonce"])
|
nonce = base64.b64decode(cipher["nonce"])
|
||||||
@@ -766,7 +783,7 @@ def read_vault(passphrase, return_key=False):
|
|||||||
plaintext = _decrypt_vault_payload(key, nonce, ciphertext, tag, VAULT_SCHEMA.encode("utf-8"))
|
plaintext = _decrypt_vault_payload(key, nonce, ciphertext, tag, VAULT_SCHEMA.encode("utf-8"))
|
||||||
data = json.loads(plaintext.decode("utf-8"))
|
data = json.loads(plaintext.decode("utf-8"))
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
return (data, key) if return_key else data
|
return (data, key) if return_key else data
|
||||||
|
|
||||||
|
|
||||||
@@ -804,7 +821,7 @@ def write_vault(passphrase, payload):
|
|||||||
|
|
||||||
def write_vault_with_key(key, payload):
|
def write_vault_with_key(key, payload):
|
||||||
if not key:
|
if not key:
|
||||||
raise RuntimeError("gateway is locked; unlock before updating vault secrets")
|
raise RuntimeError("local gateway vault key is unavailable before updating vault secrets")
|
||||||
plaintext = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
plaintext = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
nonce, ciphertext, tag = _encrypt_vault_payload(key, plaintext, VAULT_SCHEMA.encode("utf-8"))
|
nonce, ciphertext, tag = _encrypt_vault_payload(key, plaintext, VAULT_SCHEMA.encode("utf-8"))
|
||||||
envelope = json.loads(VAULT_PATH.read_text(encoding="utf-8"))
|
envelope = json.loads(VAULT_PATH.read_text(encoding="utf-8"))
|
||||||
@@ -1049,10 +1066,10 @@ def gateway_locked_reason():
|
|||||||
if not vault_exists():
|
if not vault_exists():
|
||||||
return "vault_not_configured"
|
return "vault_not_configured"
|
||||||
if not GATEWAY_UNLOCKED:
|
if not GATEWAY_UNLOCKED:
|
||||||
return "locked"
|
try:
|
||||||
if GATEWAY_LAST_USED_AT and datetime.now(timezone.utc) - GATEWAY_LAST_USED_AT > timedelta(seconds=GATEWAY_IDLE_TIMEOUT_SECONDS):
|
gateway_open_local()
|
||||||
gateway_lock()
|
except Exception:
|
||||||
return "idle_timeout"
|
return "local_vault_unavailable"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -1065,20 +1082,28 @@ def touch_gateway():
|
|||||||
GATEWAY_LAST_USED_AT = datetime.now(timezone.utc)
|
GATEWAY_LAST_USED_AT = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_gateway_runtime_caches():
|
||||||
|
global GATEWAY_PROJECT_BUNDLE_PASSPHRASE
|
||||||
|
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = None
|
||||||
|
MEMORY_SECRET_CACHE.clear()
|
||||||
|
ACTIVE_MEMORY_SECRET_VERSIONS.clear()
|
||||||
|
|
||||||
|
|
||||||
def gateway_lock():
|
def gateway_lock():
|
||||||
global VAULT_SECRETS, VAULT_KEY, GATEWAY_UNLOCKED, GATEWAY_LAST_USED_AT, AUTH, TOKEN, TOKEN_EXPIRES_AT, GATEWAY_PROJECT_BUNDLE_PASSPHRASE
|
global VAULT_SECRETS, VAULT_KEY, GATEWAY_UNLOCKED, GATEWAY_LAST_USED_AT, AUTH, TOKEN, TOKEN_EXPIRES_AT
|
||||||
with GATEWAY_LOCK:
|
with GATEWAY_LOCK:
|
||||||
VAULT_SECRETS = {}
|
clear_gateway_runtime_caches()
|
||||||
VAULT_KEY = None
|
if vault_exists():
|
||||||
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = None
|
gateway_open_local()
|
||||||
MEMORY_SECRET_CACHE.clear()
|
else:
|
||||||
ACTIVE_MEMORY_SECRET_VERSIONS.clear()
|
VAULT_SECRETS = {}
|
||||||
GATEWAY_UNLOCKED = False
|
VAULT_KEY = None
|
||||||
|
GATEWAY_UNLOCKED = False
|
||||||
GATEWAY_LAST_USED_AT = None
|
GATEWAY_LAST_USED_AT = None
|
||||||
AUTH = load_auth()
|
AUTH = load_auth()
|
||||||
TOKEN = AUTH["token"]
|
TOKEN = AUTH["token"]
|
||||||
TOKEN_EXPIRES_AT = AUTH["token_expires_at"]
|
TOKEN_EXPIRES_AT = AUTH["token_expires_at"]
|
||||||
return {"status": "locked", "vault_configured": vault_exists()}
|
return {"status": "local_available" if GATEWAY_UNLOCKED else "not_configured", "vault_configured": vault_exists()}
|
||||||
|
|
||||||
|
|
||||||
def gateway_status():
|
def gateway_status():
|
||||||
@@ -1090,7 +1115,7 @@ def gateway_status():
|
|||||||
message = "No project/token is configured for this directory."
|
message = "No project/token is configured for this directory."
|
||||||
elif not vault_exists():
|
elif not vault_exists():
|
||||||
status = "vault_not_configured"
|
status = "vault_not_configured"
|
||||||
message = "Gateway vault is not configured; run nap gateway configure."
|
message = "Local gateway storage is not configured; run nap gateway configure."
|
||||||
elif remote_enabled and listener_ready:
|
elif remote_enabled and listener_ready:
|
||||||
status = "relay_ready"
|
status = "relay_ready"
|
||||||
message = "Gateway relay listener is connected."
|
message = "Gateway relay listener is connected."
|
||||||
@@ -1109,9 +1134,10 @@ def gateway_status():
|
|||||||
"container_uuid": read_container_uuid(),
|
"container_uuid": read_container_uuid(),
|
||||||
"vault_path": str(VAULT_PATH),
|
"vault_path": str(VAULT_PATH),
|
||||||
"vault_configured": vault_exists(),
|
"vault_configured": vault_exists(),
|
||||||
|
"local_vault_available": unlocked,
|
||||||
|
"local_vault_key_path": str(LOCAL_VAULT_KEY_PATH),
|
||||||
"locked": not unlocked,
|
"locked": not unlocked,
|
||||||
"lock_reason": gateway_locked_reason(),
|
"lock_reason": gateway_locked_reason(),
|
||||||
"idle_timeout_seconds": GATEWAY_IDLE_TIMEOUT_SECONDS,
|
|
||||||
"cwd": str(pathlib.Path.cwd()),
|
"cwd": str(pathlib.Path.cwd()),
|
||||||
"base_url": BASE_URL,
|
"base_url": BASE_URL,
|
||||||
"project_id": DEFAULT_PROJECT_ID,
|
"project_id": DEFAULT_PROJECT_ID,
|
||||||
@@ -1173,22 +1199,48 @@ def ensure_gateway_worker_capability(refresh=True):
|
|||||||
|
|
||||||
def apply_vault_secrets(secrets_payload):
|
def apply_vault_secrets(secrets_payload):
|
||||||
if not isinstance(secrets_payload, dict):
|
if not isinstance(secrets_payload, dict):
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
secrets_map = secrets_payload.get("secrets", secrets_payload)
|
secrets_map = secrets_payload.get("secrets", secrets_payload)
|
||||||
if not isinstance(secrets_map, dict):
|
if not isinstance(secrets_map, dict):
|
||||||
raise ValueError("gateway unlock failed")
|
raise ValueError("local gateway vault open failed")
|
||||||
return {key: value for key, value in secrets_map.items() if value is not None}
|
return {key: value for key, value in secrets_map.items() if value is not None}
|
||||||
|
|
||||||
|
|
||||||
def gateway_unlock(passphrase):
|
def gateway_open_local(passphrase=None):
|
||||||
global VAULT_SECRETS, VAULT_KEY, GATEWAY_UNLOCKED, AUTH, BASE_URL, TOKEN, DEFAULT_PROJECT_ID, TOKEN_EXPIRES_AT, GATEWAY_PROJECT_BUNDLE_PASSPHRASE
|
global VAULT_SECRETS, VAULT_KEY, GATEWAY_UNLOCKED, AUTH, BASE_URL, TOKEN, DEFAULT_PROJECT_ID, TOKEN_EXPIRES_AT, GATEWAY_PROJECT_BUNDLE_PASSPHRASE
|
||||||
if not vault_exists():
|
if not vault_exists():
|
||||||
raise RuntimeError("gateway vault is not configured")
|
raise RuntimeError("gateway vault is not configured")
|
||||||
|
local_key = read_local_vault_key(create=False)
|
||||||
|
vault_payload = None
|
||||||
|
vault_key = None
|
||||||
|
migrated_from_passphrase = False
|
||||||
|
try:
|
||||||
|
if local_key:
|
||||||
|
try:
|
||||||
|
vault_payload, vault_key = read_vault(local_key, return_key=True)
|
||||||
|
except Exception:
|
||||||
|
if not passphrase:
|
||||||
|
raise
|
||||||
|
vault_payload, _legacy_key = read_vault(passphrase, return_key=True)
|
||||||
|
write_vault(local_key, vault_payload)
|
||||||
|
vault_payload, vault_key = read_vault(local_key, return_key=True)
|
||||||
|
migrated_from_passphrase = True
|
||||||
|
elif passphrase:
|
||||||
|
vault_payload, _legacy_key = read_vault(passphrase, return_key=True)
|
||||||
|
local_key = read_local_vault_key(create=True)
|
||||||
|
write_vault(local_key, vault_payload)
|
||||||
|
vault_payload, vault_key = read_vault(local_key, return_key=True)
|
||||||
|
migrated_from_passphrase = True
|
||||||
|
else:
|
||||||
|
raise RuntimeError("local gateway vault key is unavailable")
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
"local gateway vault is unavailable; run gateway setup/configure once with the existing passphrase to migrate local access"
|
||||||
|
) from exc
|
||||||
try:
|
try:
|
||||||
vault_payload, vault_key = read_vault(passphrase, return_key=True)
|
|
||||||
secrets_map = apply_vault_secrets(vault_payload)
|
secrets_map = apply_vault_secrets(vault_payload)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise RuntimeError("gateway unlock failed") from exc
|
raise RuntimeError("local gateway vault is unavailable") from exc
|
||||||
with GATEWAY_LOCK:
|
with GATEWAY_LOCK:
|
||||||
public = load_public_auth_file()
|
public = load_public_auth_file()
|
||||||
recovered_auth = auth_file_secret_subset(secrets_map)
|
recovered_auth = auth_file_secret_subset(secrets_map)
|
||||||
@@ -1218,22 +1270,27 @@ def gateway_unlock(passphrase):
|
|||||||
TOKEN = AUTH["token"]
|
TOKEN = AUTH["token"]
|
||||||
DEFAULT_PROJECT_ID = AUTH["project_id"]
|
DEFAULT_PROJECT_ID = AUTH["project_id"]
|
||||||
TOKEN_EXPIRES_AT = AUTH["token_expires_at"]
|
TOKEN_EXPIRES_AT = AUTH["token_expires_at"]
|
||||||
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = str(passphrase)
|
if passphrase:
|
||||||
write_gateway_relay_secret(passphrase)
|
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = str(passphrase)
|
||||||
|
write_gateway_relay_secret(passphrase)
|
||||||
persist_vault_secrets()
|
persist_vault_secrets()
|
||||||
touch_gateway()
|
touch_gateway()
|
||||||
return {
|
return {
|
||||||
"status": "unlocked",
|
"status": "local_available",
|
||||||
"message": "Gateway vault unlocked.",
|
"message": "Gateway local vault is available.",
|
||||||
"project_id": DEFAULT_PROJECT_ID,
|
"project_id": DEFAULT_PROJECT_ID,
|
||||||
"idle_timeout_seconds": GATEWAY_IDLE_TIMEOUT_SECONDS,
|
"migrated_local_key": migrated_from_passphrase,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gateway_unlock(passphrase):
|
||||||
|
return gateway_open_local(passphrase)
|
||||||
|
|
||||||
|
|
||||||
def require_unlocked(operation="operation"):
|
def require_unlocked(operation="operation"):
|
||||||
reason = gateway_locked_reason()
|
reason = gateway_locked_reason()
|
||||||
if reason:
|
if reason:
|
||||||
raise RuntimeError(f"gateway is locked; unlock with the master passphrase before {operation}")
|
raise RuntimeError(f"local gateway vault is unavailable before {operation}: {reason}")
|
||||||
touch_gateway()
|
touch_gateway()
|
||||||
|
|
||||||
|
|
||||||
@@ -1261,9 +1318,9 @@ def normalize_gateway_command(value=None):
|
|||||||
|
|
||||||
|
|
||||||
def gateway_setup(args):
|
def gateway_setup(args):
|
||||||
passphrase = args.get("passphrase") or read_master_passphrase(create=True)
|
passphrase = args.get("passphrase") or os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or read_master_passphrase(create=True)
|
||||||
if vault_exists() and not args.get("overwrite"):
|
if vault_exists() and not args.get("overwrite"):
|
||||||
result = gateway_unlock(passphrase)
|
result = gateway_open_local(passphrase)
|
||||||
capability_result = ensure_gateway_worker_capability(refresh=True)
|
capability_result = ensure_gateway_worker_capability(refresh=True)
|
||||||
if args.get("default_command"):
|
if args.get("default_command"):
|
||||||
gateway_tmux_configure({"default_command": args.get("default_command")})
|
gateway_tmux_configure({"default_command": args.get("default_command")})
|
||||||
@@ -1279,9 +1336,10 @@ def gateway_setup(args):
|
|||||||
"gateway_remote_enabled": True,
|
"gateway_remote_enabled": True,
|
||||||
}
|
}
|
||||||
payload = {"version": 1, "created_at": iso_now(), "secrets": secrets_map}
|
payload = {"version": 1, "created_at": iso_now(), "secrets": secrets_map}
|
||||||
write_vault(passphrase, payload)
|
local_key = read_local_vault_key(create=True)
|
||||||
|
write_vault(local_key, payload)
|
||||||
write_public_auth({**public, "gateway_auth_required": True, "vault_path": str(VAULT_PATH)})
|
write_public_auth({**public, "gateway_auth_required": True, "vault_path": str(VAULT_PATH)})
|
||||||
result = gateway_unlock(passphrase)
|
result = gateway_open_local(passphrase)
|
||||||
write_gateway_relay_state(
|
write_gateway_relay_state(
|
||||||
remote_enabled=True,
|
remote_enabled=True,
|
||||||
tmux_target={},
|
tmux_target={},
|
||||||
@@ -1292,11 +1350,11 @@ def gateway_setup(args):
|
|||||||
)
|
)
|
||||||
write_gateway_relay_secret(passphrase)
|
write_gateway_relay_secret(passphrase)
|
||||||
capability_result = ensure_gateway_worker_capability(refresh=True)
|
capability_result = ensure_gateway_worker_capability(refresh=True)
|
||||||
return {**gateway_status(), "status": result.get("status", "unlocked"), "message": "Gateway vault configured and unlocked.", "gateway_capability": capability_result}
|
return {**gateway_status(), "status": result.get("status", "local_available"), "message": "Gateway local vault configured.", "gateway_capability": capability_result}
|
||||||
|
|
||||||
|
|
||||||
def gateway_rotate_passphrase(args):
|
def gateway_rotate_passphrase(args):
|
||||||
require_unlocked("rotating gateway passphrase")
|
require_unlocked("rotating gateway relay passphrase")
|
||||||
new_passphrase = (
|
new_passphrase = (
|
||||||
args.get("new_passphrase")
|
args.get("new_passphrase")
|
||||||
or args.get("passphrase")
|
or args.get("passphrase")
|
||||||
@@ -1309,17 +1367,19 @@ def gateway_rotate_passphrase(args):
|
|||||||
secrets_map = dict(VAULT_SECRETS)
|
secrets_map = dict(VAULT_SECRETS)
|
||||||
secrets_map.setdefault("gateway_encryption_key", base64.b64encode(secrets.token_bytes(32)).decode("ascii"))
|
secrets_map.setdefault("gateway_encryption_key", base64.b64encode(secrets.token_bytes(32)).decode("ascii"))
|
||||||
secrets_map.setdefault("gateway_remote_enabled", True)
|
secrets_map.setdefault("gateway_remote_enabled", True)
|
||||||
write_vault(new_passphrase, {"version": 1, "updated_at": iso_now(), "secrets": secrets_map})
|
local_key = read_local_vault_key(create=True)
|
||||||
vault_payload, vault_key = read_vault(new_passphrase, return_key=True)
|
write_vault(local_key, {"version": 1, "updated_at": iso_now(), "secrets": secrets_map})
|
||||||
|
vault_payload, vault_key = read_vault(local_key, return_key=True)
|
||||||
apply_vault_secrets(vault_payload)
|
apply_vault_secrets(vault_payload)
|
||||||
globals()["VAULT_KEY"] = vault_key
|
globals()["VAULT_KEY"] = vault_key
|
||||||
globals()["VAULT_SECRETS"] = gateway_vault_secret_subset(secrets_map)
|
globals()["VAULT_SECRETS"] = gateway_vault_secret_subset(secrets_map)
|
||||||
|
globals()["GATEWAY_PROJECT_BUNDLE_PASSPHRASE"] = new_passphrase
|
||||||
write_gateway_relay_secret(new_passphrase)
|
write_gateway_relay_secret(new_passphrase)
|
||||||
touch_gateway()
|
touch_gateway()
|
||||||
return {
|
return {
|
||||||
**gateway_status(),
|
**gateway_status(),
|
||||||
"status": "rotated",
|
"status": "rotated",
|
||||||
"message": "Gateway passphrase rotated. Update NAPSEER_GATEWAY_PASSPHRASE in compose.yaml or the service environment before restarting the gateway.",
|
"message": "Gateway relay passphrase rotated. Update NAPSEER_GATEWAY_PASSPHRASE in compose.yaml or the service environment before restarting the gateway.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3087,7 +3147,7 @@ def project_bundle_passphrase():
|
|||||||
value = os.environ.get("NAPSEER_MASTER_PASSPHRASE")
|
value = os.environ.get("NAPSEER_MASTER_PASSPHRASE")
|
||||||
if value:
|
if value:
|
||||||
return value
|
return value
|
||||||
raise RuntimeError("gateway is locked; unlock before handling project key bundles")
|
raise RuntimeError("gateway relay passphrase is required before handling project key bundles")
|
||||||
|
|
||||||
|
|
||||||
def project_bundle_hkdf_info(project_id, secret_kind, wrapping_epoch, bundle_version, data_key_epoch):
|
def project_bundle_hkdf_info(project_id, secret_kind, wrapping_epoch, bundle_version, data_key_epoch):
|
||||||
@@ -3909,7 +3969,7 @@ def handle_gateway_relay_session(connect_request, listener_sock=None):
|
|||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
reason="missing_gateway_relay_secret",
|
reason="missing_gateway_relay_secret",
|
||||||
)
|
)
|
||||||
raise RuntimeError("gateway relay secret is missing; run nap gateway configure or nap gateway unlock once")
|
raise RuntimeError("gateway relay secret is missing; run nap gateway configure once")
|
||||||
spake2_context = connect_request.get("spake2_context") or {}
|
spake2_context = connect_request.get("spake2_context") or {}
|
||||||
relay_socket = str(connect_request.get("relay_socket") or connect_request.get("relay_lane") or spake2_context.get("relay_socket") or spake2_context.get("relay_lane") or "terminal")
|
relay_socket = str(connect_request.get("relay_socket") or connect_request.get("relay_lane") or spake2_context.get("relay_socket") or spake2_context.get("relay_lane") or "terminal")
|
||||||
sock = ws_connect(relay_ws_url(project_id, session_id, connect_request["relay_ticket"], relay_socket))
|
sock = ws_connect(relay_ws_url(project_id, session_id, connect_request["relay_ticket"], relay_socket))
|
||||||
@@ -6457,7 +6517,7 @@ def gateway_service_preregister(args=None):
|
|||||||
"default_command": args.get("default_command") or os.environ.get("NAPSEER_GATEWAY_COMMAND"),
|
"default_command": args.get("default_command") or os.environ.get("NAPSEER_GATEWAY_COMMAND"),
|
||||||
})
|
})
|
||||||
elif passphrase and not gateway_is_unlocked():
|
elif passphrase and not gateway_is_unlocked():
|
||||||
gateway_unlock(passphrase)
|
gateway_open_local(passphrase)
|
||||||
|
|
||||||
key_path = ensure_local_files()
|
key_path = ensure_local_files()
|
||||||
public_key = pathlib.Path(str(key_path) + ".pub").read_text(encoding="utf-8").strip()
|
public_key = pathlib.Path(str(key_path) + ".pub").read_text(encoding="utf-8").strip()
|
||||||
@@ -6569,13 +6629,13 @@ def gateway_service_activate(args=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def gateway_service_unlock_from_env():
|
def gateway_service_open_local_from_env():
|
||||||
if not vault_exists() or gateway_is_unlocked():
|
if not vault_exists() or gateway_is_unlocked():
|
||||||
return False
|
return False
|
||||||
passphrase = os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or os.environ.get("NAPSEER_GATEWAY_PASSKEY")
|
passphrase = os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or os.environ.get("NAPSEER_GATEWAY_PASSKEY")
|
||||||
if not passphrase:
|
if not passphrase:
|
||||||
return False
|
return False
|
||||||
gateway_unlock(passphrase)
|
gateway_open_local(passphrase)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -6594,7 +6654,7 @@ def gateway_service_run(args=None):
|
|||||||
or os.environ.get("NAPSEER_SERVICE_BOOTSTRAP_TOKEN")
|
or os.environ.get("NAPSEER_SERVICE_BOOTSTRAP_TOKEN")
|
||||||
or os.environ.get("NAPSEER_GATEWAY_BOOTSTRAP_TOKEN")
|
or os.environ.get("NAPSEER_GATEWAY_BOOTSTRAP_TOKEN")
|
||||||
)
|
)
|
||||||
gateway_service_unlock_from_env()
|
gateway_service_open_local_from_env()
|
||||||
if bootstrap_token and not TOKEN:
|
if bootstrap_token and not TOKEN:
|
||||||
try:
|
try:
|
||||||
gateway_service_preregister(args)
|
gateway_service_preregister(args)
|
||||||
@@ -6609,7 +6669,7 @@ def gateway_service_run(args=None):
|
|||||||
activated = False
|
activated = False
|
||||||
while not TOKEN and time.time() < deadline:
|
while not TOKEN and time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
gateway_service_unlock_from_env()
|
gateway_service_open_local_from_env()
|
||||||
gateway_service_activate(args)
|
gateway_service_activate(args)
|
||||||
activated = True
|
activated = True
|
||||||
break
|
break
|
||||||
@@ -6621,7 +6681,7 @@ def gateway_service_run(args=None):
|
|||||||
if not TOKEN and not activated:
|
if not TOKEN and not activated:
|
||||||
raise RuntimeError("gateway service was not accepted before activation timeout")
|
raise RuntimeError("gateway service was not accepted before activation timeout")
|
||||||
if not gateway_is_unlocked():
|
if not gateway_is_unlocked():
|
||||||
gateway_service_unlock_from_env()
|
gateway_service_open_local_from_env()
|
||||||
result = start_local_ui({"open_browser": False, "port": int(args.get("port") or os.environ.get("NAPSEER_GATEWAY_PORT", "0"))})
|
result = start_local_ui({"open_browser": False, "port": int(args.get("port") or os.environ.get("NAPSEER_GATEWAY_PORT", "0"))})
|
||||||
print(json.dumps(result, indent=2))
|
print(json.dumps(result, indent=2))
|
||||||
try:
|
try:
|
||||||
@@ -6707,7 +6767,7 @@ def bootstrap_project(args):
|
|||||||
if encryption == "encrypted":
|
if encryption == "encrypted":
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"CLI direct project encryption enable is disabled for backend-owned HashiCorp project secrets. "
|
"CLI direct project encryption enable is disabled for backend-owned HashiCorp project secrets. "
|
||||||
"Create the project, configure/unlock the gateway vault, then run `nap gateway vault process`."
|
"Create the project, configure the local gateway, then run `nap gateway vault process`."
|
||||||
)
|
)
|
||||||
project, project_status, message = create_project_with_state(args)
|
project, project_status, message = create_project_with_state(args)
|
||||||
encryption_state = "plaintext"
|
encryption_state = "plaintext"
|
||||||
@@ -6745,7 +6805,7 @@ def project_encryption_transition(args=None):
|
|||||||
if desired == "encrypted":
|
if desired == "encrypted":
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"CLI direct project encryption enable is disabled for backend-owned HashiCorp project secrets. "
|
"CLI direct project encryption enable is disabled for backend-owned HashiCorp project secrets. "
|
||||||
"Create the project, configure/unlock the gateway vault, then run `nap gateway vault process`."
|
"Create the project, configure the local gateway, then run `nap gateway vault process`."
|
||||||
)
|
)
|
||||||
project_id = resolve_project_id(args)
|
project_id = resolve_project_id(args)
|
||||||
operation = "disable"
|
operation = "disable"
|
||||||
@@ -6908,7 +6968,7 @@ def cli_gateway_configure(args):
|
|||||||
default_command = cli_option(args, "--command", default=cli_option(args, "--default-command", default=None))
|
default_command = cli_option(args, "--command", default=cli_option(args, "--default-command", default=None))
|
||||||
if bootstrap_token:
|
if bootstrap_token:
|
||||||
if passphrase and vault_exists() and not gateway_is_unlocked():
|
if passphrase and vault_exists() and not gateway_is_unlocked():
|
||||||
gateway_unlock(passphrase)
|
gateway_open_local(passphrase)
|
||||||
if TOKEN:
|
if TOKEN:
|
||||||
if default_command:
|
if default_command:
|
||||||
gateway_tmux_configure({"default_command": default_command})
|
gateway_tmux_configure({"default_command": default_command})
|
||||||
@@ -6931,9 +6991,11 @@ def cli_gateway_configure(args):
|
|||||||
return gateway_bootstrap_token_invalid_result()
|
return gateway_bootstrap_token_invalid_result()
|
||||||
|
|
||||||
if not vault_exists():
|
if not vault_exists():
|
||||||
|
if not passphrase:
|
||||||
|
raise RuntimeError("gateway relay passphrase is required to create gateway storage")
|
||||||
gateway_setup({"passphrase": passphrase})
|
gateway_setup({"passphrase": passphrase})
|
||||||
if not gateway_is_unlocked():
|
if not gateway_is_unlocked():
|
||||||
gateway_unlock(passphrase or getpass.getpass("Gateway passphrase: "))
|
gateway_open_local(passphrase)
|
||||||
|
|
||||||
target = parse_tmux_target(cli_option(args, "--target", default=""))
|
target = parse_tmux_target(cli_option(args, "--target", default=""))
|
||||||
command = default_command or ""
|
command = default_command or ""
|
||||||
@@ -6981,7 +7043,7 @@ def mcp_gateway_configure(args):
|
|||||||
default_command = args.get("default_command") or args.get("command")
|
default_command = args.get("default_command") or args.get("command")
|
||||||
if bootstrap_token:
|
if bootstrap_token:
|
||||||
if passphrase and vault_exists() and not gateway_is_unlocked():
|
if passphrase and vault_exists() and not gateway_is_unlocked():
|
||||||
gateway_unlock(passphrase)
|
gateway_open_local(passphrase)
|
||||||
if TOKEN:
|
if TOKEN:
|
||||||
if default_command:
|
if default_command:
|
||||||
gateway_tmux_configure({"default_command": default_command})
|
gateway_tmux_configure({"default_command": default_command})
|
||||||
@@ -7010,7 +7072,7 @@ def mcp_gateway_configure(args):
|
|||||||
raise RuntimeError("gateway passphrase is required to create the gateway vault")
|
raise RuntimeError("gateway passphrase is required to create the gateway vault")
|
||||||
gateway_setup({"passphrase": passphrase, "default_command": default_command})
|
gateway_setup({"passphrase": passphrase, "default_command": default_command})
|
||||||
elif passphrase and not gateway_is_unlocked():
|
elif passphrase and not gateway_is_unlocked():
|
||||||
gateway_unlock(passphrase)
|
gateway_open_local(passphrase)
|
||||||
|
|
||||||
target = parse_tmux_target(args.get("target", ""))
|
target = parse_tmux_target(args.get("target", ""))
|
||||||
payload = {
|
payload = {
|
||||||
@@ -7028,7 +7090,7 @@ def cli_gateway_require_unlocked(args, operation="gateway operation"):
|
|||||||
if not vault_exists():
|
if not vault_exists():
|
||||||
raise RuntimeError("gateway vault is not configured; run nap gateway configure first")
|
raise RuntimeError("gateway vault is not configured; run nap gateway configure first")
|
||||||
if not gateway_is_unlocked():
|
if not gateway_is_unlocked():
|
||||||
gateway_unlock(cli_option(args, "--passphrase", default=None) or getpass.getpass("Gateway passphrase: "))
|
gateway_open_local(cli_option(args, "--passphrase", default=None))
|
||||||
require_unlocked(operation)
|
require_unlocked(operation)
|
||||||
|
|
||||||
|
|
||||||
@@ -9458,9 +9520,10 @@ def start_local_ui(args):
|
|||||||
def handle_local_api_post(self, route, payload):
|
def handle_local_api_post(self, route, payload):
|
||||||
project_id = current_project_id()
|
project_id = current_project_id()
|
||||||
if route == "/local-api/gateway/unlock":
|
if route == "/local-api/gateway/unlock":
|
||||||
return gateway_unlock(payload.get("passphrase", ""))
|
return gateway_open_local(payload.get("passphrase", ""))
|
||||||
if route == "/local-api/gateway/lock":
|
if route == "/local-api/gateway/lock":
|
||||||
return gateway_lock()
|
clear_gateway_runtime_caches()
|
||||||
|
return gateway_status()
|
||||||
if route == "/local-api/gateway/setup":
|
if route == "/local-api/gateway/setup":
|
||||||
return gateway_setup(payload)
|
return gateway_setup(payload)
|
||||||
if route == "/local-api/gateway/configure":
|
if route == "/local-api/gateway/configure":
|
||||||
@@ -9574,7 +9637,7 @@ def start_local_ui(args):
|
|||||||
<label>Project slug<input name="slug" required value="{html.escape(default_project_slug())}"></label>
|
<label>Project slug<input name="slug" required value="{html.escape(default_project_slug())}"></label>
|
||||||
<label>Description<textarea name="description"></textarea></label>
|
<label>Description<textarea name="description"></textarea></label>
|
||||||
<input type="hidden" name="encryption" value="plaintext">
|
<input type="hidden" name="encryption" value="plaintext">
|
||||||
<p>Project encryption is configured after project creation from an unlocked gateway with <code>nap gateway vault process</code>.</p>
|
<p>Project encryption is configured after project creation from the local gateway with <code>nap gateway vault process</code>.</p>
|
||||||
<button type="submit">Create project</button></form></section></main></body></html>""")
|
<button type="submit">Create project</button></form></section></main></body></html>""")
|
||||||
return
|
return
|
||||||
search = query.get("q", [""])[0]
|
search = query.get("q", [""])[0]
|
||||||
@@ -9690,9 +9753,10 @@ def start_local_ui(args):
|
|||||||
if route == "/gateway/setup":
|
if route == "/gateway/setup":
|
||||||
result = gateway_setup(payload)
|
result = gateway_setup(payload)
|
||||||
elif route == "/gateway/unlock":
|
elif route == "/gateway/unlock":
|
||||||
result = gateway_unlock(payload.get("passphrase", ""))
|
result = gateway_open_local(payload.get("passphrase", ""))
|
||||||
elif route == "/gateway/lock":
|
elif route == "/gateway/lock":
|
||||||
result = gateway_lock()
|
clear_gateway_runtime_caches()
|
||||||
|
result = gateway_status()
|
||||||
elif route == "/gateway/tmux/configure":
|
elif route == "/gateway/tmux/configure":
|
||||||
result = gateway_tmux_configure(payload)
|
result = gateway_tmux_configure(payload)
|
||||||
elif route == "/gateway/terminal/restart":
|
elif route == "/gateway/terminal/restart":
|
||||||
@@ -9918,17 +9982,17 @@ def raw_tools():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_gateway_status",
|
"name": "nap_gateway_status",
|
||||||
"description": "Show whether the encrypted local gateway vault is configured and unlocked. Does not reveal secrets.",
|
"description": "Show local gateway, relay, and project status. Does not reveal secrets.",
|
||||||
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_gateway_setup",
|
"name": "nap_gateway_setup",
|
||||||
"description": "Create or migrate the encrypted local gateway vault using a master passphrase and default managed agent command.",
|
"description": "Create or migrate local gateway storage and configure the remote relay passphrase.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["passphrase"],
|
"required": ["passphrase"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"passphrase": {"type": "string", "description": "Master passphrase used only locally to encrypt the gateway vault."},
|
"passphrase": {"type": "string", "description": "Remote relay passphrase. Local access is protected by the machine/user session, not an interactive unlock gate."},
|
||||||
"default_command": {"type": "string", "description": "Command Napseer should launch in its managed tmux window, for example bash -l."},
|
"default_command": {"type": "string", "description": "Command Napseer should launch in its managed tmux window, for example bash -l."},
|
||||||
"overwrite": {"type": "boolean"},
|
"overwrite": {"type": "boolean"},
|
||||||
},
|
},
|
||||||
@@ -9966,7 +10030,7 @@ def raw_tools():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_gateway_vault_process",
|
"name": "nap_gateway_vault_process",
|
||||||
"description": "Complete pending project vault setup requests assigned to this unlocked gateway by uploading opaque client-wrapped key bundle records for backend-owned HashiCorp storage.",
|
"description": "Complete pending project vault setup requests assigned to this local gateway by uploading opaque client-wrapped key bundle records for backend-owned HashiCorp storage.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -9978,7 +10042,7 @@ def raw_tools():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_gateway_vault_secret_rotate",
|
"name": "nap_gateway_vault_secret_rotate",
|
||||||
"description": "Rotate one project secret version from the unlocked gateway by uploading an opaque client-wrapped key bundle record for backend-owned HashiCorp storage.",
|
"description": "Rotate one project secret version from this local gateway by uploading an opaque client-wrapped key bundle record for backend-owned HashiCorp storage.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["secret_kind"],
|
"required": ["secret_kind"],
|
||||||
@@ -9989,21 +10053,6 @@ def raw_tools():
|
|||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "nap_gateway_unlock",
|
|
||||||
"description": "Unlock the encrypted local gateway vault with the master passphrase.",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["passphrase"],
|
|
||||||
"properties": {"passphrase": {"type": "string"}},
|
|
||||||
"additionalProperties": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nap_gateway_lock",
|
|
||||||
"description": "Lock the local gateway and clear decrypted secrets from memory.",
|
|
||||||
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "nap_gateway_restart",
|
"name": "nap_gateway_restart",
|
||||||
"description": "Kill and relaunch the managed gateway tmux window using the configured default command.",
|
"description": "Kill and relaunch the managed gateway tmux window using the configured default command.",
|
||||||
@@ -10918,10 +10967,8 @@ TOOL_CATEGORIES = {
|
|||||||
"nap_gateway_vault_ls",
|
"nap_gateway_vault_ls",
|
||||||
"nap_gateway_vault_process",
|
"nap_gateway_vault_process",
|
||||||
"nap_gateway_vault_secret_rotate",
|
"nap_gateway_vault_secret_rotate",
|
||||||
"nap_gateway_unlock",
|
|
||||||
"nap_gateway_restart",
|
"nap_gateway_restart",
|
||||||
"nap_gateway_kill",
|
"nap_gateway_kill",
|
||||||
"nap_gateway_lock",
|
|
||||||
"nap_ui_open",
|
"nap_ui_open",
|
||||||
],
|
],
|
||||||
"maintenance": [
|
"maintenance": [
|
||||||
@@ -10993,13 +11040,11 @@ LOCAL_FILE_TOOLS = {
|
|||||||
"nap_gateway_configure",
|
"nap_gateway_configure",
|
||||||
"nap_gateway_vault_process",
|
"nap_gateway_vault_process",
|
||||||
"nap_gateway_vault_secret_rotate",
|
"nap_gateway_vault_secret_rotate",
|
||||||
"nap_gateway_unlock",
|
|
||||||
"nap_gateway_lock",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PROCESS_CONTROL_TOOLS = {"nap_ui_open"}
|
PROCESS_CONTROL_TOOLS = {"nap_ui_open"}
|
||||||
TERMINAL_CONTROL_TOOLS = {"nap_gateway_restart", "nap_gateway_kill"}
|
TERMINAL_CONTROL_TOOLS = {"nap_gateway_restart", "nap_gateway_kill"}
|
||||||
SECRET_ACCEPTING_TOOLS = {"nap_gateway_setup", "nap_gateway_unlock"}
|
SECRET_ACCEPTING_TOOLS = {"nap_gateway_setup", "nap_gateway_configure"}
|
||||||
AUTO_LOCK_TOOLS = {
|
AUTO_LOCK_TOOLS = {
|
||||||
"nap_create_node",
|
"nap_create_node",
|
||||||
"nap_tee",
|
"nap_tee",
|
||||||
@@ -11045,7 +11090,7 @@ def tool_auth_mode(name):
|
|||||||
if name in {"nap_apropos", "nap_man", "nap_contract", "nap_update_status"}:
|
if name in {"nap_apropos", "nap_man", "nap_contract", "nap_update_status"}:
|
||||||
return "public"
|
return "public"
|
||||||
if category == "gateway":
|
if category == "gateway":
|
||||||
return "gateway-vault" if name in SECRET_ACCEPTING_TOOLS or name.startswith("nap_gateway_") else "local-only"
|
return "local-gateway"
|
||||||
if name in {"nap_grep_local", "nap_index_status", "nap_index_clear", "nap_reindex_project", "nap_ui_open", "nap_update_self"}:
|
if name in {"nap_grep_local", "nap_index_status", "nap_index_clear", "nap_reindex_project", "nap_ui_open", "nap_update_self"}:
|
||||||
return "local-only"
|
return "local-only"
|
||||||
if category in {"explore", "update", "coordination", "agent", "project", "schedules", "maintenance"}:
|
if category in {"explore", "update", "coordination", "agent", "project", "schedules", "maintenance"}:
|
||||||
@@ -11268,14 +11313,10 @@ def call_tool_impl(name, args):
|
|||||||
return gateway_process_vault_setup_requests(args)
|
return gateway_process_vault_setup_requests(args)
|
||||||
if name == "nap_gateway_vault_secret_rotate":
|
if name == "nap_gateway_vault_secret_rotate":
|
||||||
return gateway_rotate_project_vault_secret(args)
|
return gateway_rotate_project_vault_secret(args)
|
||||||
if name == "nap_gateway_unlock":
|
|
||||||
return gateway_unlock(args.get("passphrase", ""))
|
|
||||||
if name == "nap_gateway_restart":
|
if name == "nap_gateway_restart":
|
||||||
return gateway_terminal_restart(args)
|
return gateway_terminal_restart(args)
|
||||||
if name == "nap_gateway_kill":
|
if name == "nap_gateway_kill":
|
||||||
return gateway_terminal_kill(args)
|
return gateway_terminal_kill(args)
|
||||||
if name == "nap_gateway_lock":
|
|
||||||
return gateway_lock()
|
|
||||||
if name == "nap_login":
|
if name == "nap_login":
|
||||||
return renew_auth()
|
return renew_auth()
|
||||||
if name == "nap_update_status":
|
if name == "nap_update_status":
|
||||||
@@ -11762,13 +11803,7 @@ def cli_main(argv):
|
|||||||
"new_passphrase": cli_option(args, "--new-passphrase", "--passphrase", default=None),
|
"new_passphrase": cli_option(args, "--new-passphrase", "--passphrase", default=None),
|
||||||
}), indent=2))
|
}), indent=2))
|
||||||
return
|
return
|
||||||
if subcommand == "unlock":
|
print("Usage: napseer_mcp_server.py gateway [status|configure [--command CMD] [--target session:window.pane]|vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all]|terminal [list|create|close|capture|input]|schedule [list|create|update|delete|run]|restart|kill|setup|rotate-passphrase --new-passphrase TEXT]")
|
||||||
print(json.dumps(gateway_unlock(cli_option(args, "--passphrase", default=None) or getpass.getpass("Gateway passphrase: ")), indent=2))
|
|
||||||
return
|
|
||||||
if subcommand == "lock":
|
|
||||||
print(json.dumps(gateway_lock(), indent=2))
|
|
||||||
return
|
|
||||||
print("Usage: napseer_mcp_server.py gateway [status|configure [--command CMD] [--target session:window.pane]|vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all]|terminal [list|create|close|capture|input]|schedule [list|create|update|delete|run]|restart [--passphrase TEXT]|kill [--passphrase TEXT]|setup|rotate-passphrase --new-passphrase TEXT|unlock|lock]")
|
|
||||||
return
|
return
|
||||||
if command == "feedback":
|
if command == "feedback":
|
||||||
subcommand = argv[2] if len(argv) > 2 else "list"
|
subcommand = argv[2] if len(argv) > 2 else "list"
|
||||||
|
|||||||
@@ -591,18 +591,18 @@ def handle_gateway(args):
|
|||||||
subcommand = args[0] if args else "ls"
|
subcommand = args[0] if args else "ls"
|
||||||
rest = args[1:] if args else []
|
rest = args[1:] if args else []
|
||||||
if subcommand in {"help", "-h", "--help"}:
|
if subcommand in {"help", "-h", "--help"}:
|
||||||
print("Usage: nap gateway [start|stop|status|logs|configure|setup|unlock|lock|restart|kill|vault|terminal|schedule|service]")
|
print("Usage: nap gateway [start|stop|status|logs|configure|setup|restart|kill|vault|terminal|schedule|service]")
|
||||||
print(" nap gateway start [--port PORT] Start the local gateway service.")
|
print(" nap gateway start [--port PORT] Start the local gateway service.")
|
||||||
print(" nap gateway status Show the managed gateway process.")
|
print(" nap gateway status Show the managed gateway process.")
|
||||||
print(" nap gateway configure [--command CMD] Configure the local gateway command.")
|
print(" nap gateway configure [--command CMD] Configure the local gateway command.")
|
||||||
print(" nap gateway setup Create or unlock the encrypted gateway vault.")
|
print(" nap gateway setup Create or migrate local gateway storage.")
|
||||||
print(" nap gateway vault Show gateway vault status and pending setup requests.")
|
print(" nap gateway vault Show gateway vault status and pending setup requests.")
|
||||||
print(" nap gateway vault process Complete pending setup requests.")
|
print(" nap gateway vault process Complete pending setup requests.")
|
||||||
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway|memory")
|
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway|memory")
|
||||||
return 0
|
return 0
|
||||||
if subcommand == "vault" and rest and rest[0] in {"help", "-h", "--help"}:
|
if subcommand == "vault" and rest and rest[0] in {"help", "-h", "--help"}:
|
||||||
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all] [--project-id ID]")
|
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway|memory] [--all] [--project-id ID]")
|
||||||
print(" status Show configured/locked state and pending setup requests.")
|
print(" status Show local gateway state and pending setup requests.")
|
||||||
print(" process Upload opaque client-wrapped key bundle records for backend-owned HashiCorp storage.")
|
print(" process Upload opaque client-wrapped key bundle records for backend-owned HashiCorp storage.")
|
||||||
print(" rotate-secret Rotate one encrypted project secret.")
|
print(" rotate-secret Rotate one encrypted project secret.")
|
||||||
return 0
|
return 0
|
||||||
@@ -616,9 +616,9 @@ def handle_gateway(args):
|
|||||||
return print_json(service_logs("gateway", rest)) or 0
|
return print_json(service_logs("gateway", rest)) or 0
|
||||||
if subcommand == "configure":
|
if subcommand == "configure":
|
||||||
return run_wrapper("gateway", ["configure", *rest])
|
return run_wrapper("gateway", ["configure", *rest])
|
||||||
if subcommand in {"setup", "unlock", "lock", "restart", "kill", "vault", "terminal", "schedule", "service"}:
|
if subcommand in {"setup", "restart", "kill", "vault", "terminal", "schedule", "service"}:
|
||||||
return run_wrapper("gateway", [subcommand, *rest])
|
return run_wrapper("gateway", [subcommand, *rest])
|
||||||
raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|unlock|lock|restart|kill|logs|status|vault|terminal|schedule|service")
|
raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|restart|kill|logs|status|vault|terminal|schedule|service")
|
||||||
|
|
||||||
|
|
||||||
def print_json(value):
|
def print_json(value):
|
||||||
@@ -675,8 +675,8 @@ def main(argv):
|
|||||||
print(" Example: nap auth login-local")
|
print(" Example: nap auth login-local")
|
||||||
print(" project Project commands: create, status, encryption.")
|
print(" project Project commands: create, status, encryption.")
|
||||||
print(" Examples: nap project create, nap project status, nap project encryption status")
|
print(" Examples: nap project create, nap project status, nap project encryption status")
|
||||||
print(" Encrypted project setup is completed by an unlocked gateway: nap gateway vault process")
|
print(" Encrypted project setup is completed by the local gateway: nap gateway vault process")
|
||||||
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, restart, kill, logs, status, vault.")
|
print(" gateway Gateway commands: start, stop, configure, setup, restart, kill, logs, status, vault.")
|
||||||
print(" Start auto-selects a local port; use --port to pin one.")
|
print(" Start auto-selects a local port; use --port to pin one.")
|
||||||
print(" Examples: nap gateway start, nap gateway vault, nap gateway vault rotate-secret --kind chat")
|
print(" Examples: nap gateway start, nap gateway vault, nap gateway vault rotate-secret --kind chat")
|
||||||
print(" agent Agent commands: list, workspaces, create, show, edit.")
|
print(" agent Agent commands: list, workspaces, create, show, edit.")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def assert_equal(actual, expected, message):
|
|||||||
|
|
||||||
def run():
|
def run():
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
calls = {"preregister": [], "setup": [], "unlock": [], "tmux": []}
|
calls = {"preregister": [], "setup": [], "open": [], "tmux": []}
|
||||||
|
|
||||||
mod.AUTH = {}
|
mod.AUTH = {}
|
||||||
mod.TOKEN = None
|
mod.TOKEN = None
|
||||||
@@ -32,7 +32,7 @@ def run():
|
|||||||
mod.vault_exists = lambda: False
|
mod.vault_exists = lambda: False
|
||||||
mod.gateway_is_unlocked = lambda: False
|
mod.gateway_is_unlocked = lambda: False
|
||||||
mod.gateway_setup = lambda payload: calls["setup"].append(payload) or {"status": "configured"}
|
mod.gateway_setup = lambda payload: calls["setup"].append(payload) or {"status": "configured"}
|
||||||
mod.gateway_unlock = lambda passphrase: calls["unlock"].append(passphrase) or {"status": "unlocked"}
|
mod.gateway_open_local = lambda passphrase=None: calls["open"].append(passphrase) or {"status": "local_available"}
|
||||||
mod.gateway_tmux_configure = lambda payload: calls["tmux"].append(payload) or {"status": "configured"}
|
mod.gateway_tmux_configure = lambda payload: calls["tmux"].append(payload) or {"status": "configured"}
|
||||||
mod.gateway_status = lambda: {"status": "ok"}
|
mod.gateway_status = lambda: {"status": "ok"}
|
||||||
mod.gateway_log = lambda *args, **kwargs: None
|
mod.gateway_log = lambda *args, **kwargs: None
|
||||||
@@ -83,19 +83,19 @@ def run():
|
|||||||
assert_equal(invalid_cli["status"], "bootstrap_token_invalid", "cli invalid token fallback")
|
assert_equal(invalid_cli["status"], "bootstrap_token_invalid", "cli invalid token fallback")
|
||||||
|
|
||||||
calls["preregister"].clear()
|
calls["preregister"].clear()
|
||||||
calls["unlock"].clear()
|
calls["open"].clear()
|
||||||
calls["tmux"].clear()
|
calls["tmux"].clear()
|
||||||
mod.gateway_service_preregister = preregister
|
mod.gateway_service_preregister = preregister
|
||||||
mod.TOKEN = None
|
mod.TOKEN = None
|
||||||
mod.vault_exists = lambda: True
|
mod.vault_exists = lambda: True
|
||||||
mod.gateway_is_unlocked = lambda: False
|
mod.gateway_is_unlocked = lambda: False
|
||||||
|
|
||||||
def unlock_existing(passphrase):
|
def open_existing(passphrase=None):
|
||||||
calls["unlock"].append(passphrase)
|
calls["open"].append(passphrase)
|
||||||
mod.TOKEN = "np_existing_worker"
|
mod.TOKEN = "np_existing_worker"
|
||||||
return {"status": "unlocked"}
|
return {"status": "local_available"}
|
||||||
|
|
||||||
mod.gateway_unlock = unlock_existing
|
mod.gateway_open_local = open_existing
|
||||||
preserved_mcp = mod.mcp_gateway_configure({
|
preserved_mcp = mod.mcp_gateway_configure({
|
||||||
"bootstrap_token": "npb_existing",
|
"bootstrap_token": "npb_existing",
|
||||||
"passphrase": "pw_existing",
|
"passphrase": "pw_existing",
|
||||||
@@ -109,31 +109,31 @@ def run():
|
|||||||
assert_equal(preserved_mcp["status"], "configured", "mcp preserved gateway should skip preregister")
|
assert_equal(preserved_mcp["status"], "configured", "mcp preserved gateway should skip preregister")
|
||||||
assert_equal(preserved_cli["status"], "configured", "cli preserved gateway should skip preregister")
|
assert_equal(preserved_cli["status"], "configured", "cli preserved gateway should skip preregister")
|
||||||
assert_equal(calls["preregister"], [], "preserved gateway preregister calls")
|
assert_equal(calls["preregister"], [], "preserved gateway preregister calls")
|
||||||
assert_equal(calls["unlock"], ["pw_existing", "pw_existing"], "preserved gateway unlocks before preregister")
|
assert_equal(calls["open"], ["pw_existing", "pw_existing"], "preserved gateway opens local storage before preregister")
|
||||||
assert_equal(len(calls["tmux"]), 2, "preserved gateway command updates")
|
assert_equal(len(calls["tmux"]), 2, "preserved gateway command updates")
|
||||||
|
|
||||||
calls["preregister"].clear()
|
calls["preregister"].clear()
|
||||||
calls["unlock"].clear()
|
calls["open"].clear()
|
||||||
mod.TOKEN = None
|
mod.TOKEN = None
|
||||||
mod.vault_exists = lambda: True
|
mod.vault_exists = lambda: True
|
||||||
service_unlocked = {"value": False}
|
service_available = {"value": False}
|
||||||
mod.gateway_is_unlocked = lambda: service_unlocked["value"]
|
mod.gateway_is_unlocked = lambda: service_available["value"]
|
||||||
os.environ["NAPSEER_GATEWAY_PASSPHRASE"] = "pw_service"
|
os.environ["NAPSEER_GATEWAY_PASSPHRASE"] = "pw_service"
|
||||||
os.environ["NAPSEER_GATEWAY_BOOTSTRAP_TOKEN"] = "npb_service"
|
os.environ["NAPSEER_GATEWAY_BOOTSTRAP_TOKEN"] = "npb_service"
|
||||||
mod.start_local_ui = lambda payload: {"status": "started", "payload": payload}
|
mod.start_local_ui = lambda payload: {"status": "started", "payload": payload}
|
||||||
|
|
||||||
def service_unlock(passphrase):
|
def service_open(passphrase=None):
|
||||||
calls["unlock"].append(passphrase)
|
calls["open"].append(passphrase)
|
||||||
mod.TOKEN = "np_existing_worker"
|
mod.TOKEN = "np_existing_worker"
|
||||||
service_unlocked["value"] = True
|
service_available["value"] = True
|
||||||
return {"status": "unlocked"}
|
return {"status": "local_available"}
|
||||||
|
|
||||||
mod.gateway_unlock = service_unlock
|
mod.gateway_open_local = service_open
|
||||||
mod.threading.Event = lambda: type("StopEvent", (), {"wait": lambda self: (_ for _ in ()).throw(KeyboardInterrupt())})()
|
mod.threading.Event = lambda: type("StopEvent", (), {"wait": lambda self: (_ for _ in ()).throw(KeyboardInterrupt())})()
|
||||||
service_result = mod.gateway_service_run({"timeout_seconds": 0})
|
service_result = mod.gateway_service_run({"timeout_seconds": 0})
|
||||||
assert_equal(service_result["status"], "started", "service run with preserved token")
|
assert_equal(service_result["status"], "started", "service run with preserved token")
|
||||||
assert_equal(calls["preregister"], [], "service run preserved gateway preregister calls")
|
assert_equal(calls["preregister"], [], "service run preserved gateway preregister calls")
|
||||||
assert_equal(calls["unlock"], ["pw_service"], "service run unlocks before preregister")
|
assert_equal(calls["open"], ["pw_service"], "service run opens local storage before preregister")
|
||||||
|
|
||||||
print("ok: gateway configure CLI/MCP parity smoke passed")
|
print("ok: gateway configure CLI/MCP parity smoke passed")
|
||||||
|
|
||||||
|
|||||||
+10
-14
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Smoke-test auth refresh while the gateway vault is locked."""
|
"""Smoke-test auth refresh with local gateway storage available."""
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -20,8 +20,9 @@ def run():
|
|||||||
mod = load_module()
|
mod = load_module()
|
||||||
writes = []
|
writes = []
|
||||||
|
|
||||||
|
vault_writes = []
|
||||||
mod.VAULT_SECRETS = {}
|
mod.VAULT_SECRETS = {}
|
||||||
mod.VAULT_KEY = None
|
mod.VAULT_KEY = b"k" * 32
|
||||||
mod.AUTH = {
|
mod.AUTH = {
|
||||||
"base_url": "https://api.example.test",
|
"base_url": "https://api.example.test",
|
||||||
"project_id": "project_1",
|
"project_id": "project_1",
|
||||||
@@ -35,7 +36,7 @@ def run():
|
|||||||
mod.TOKEN = "expired"
|
mod.TOKEN = "expired"
|
||||||
mod.TOKEN_EXPIRES_AT = "old"
|
mod.TOKEN_EXPIRES_AT = "old"
|
||||||
mod.vault_exists = lambda: True
|
mod.vault_exists = lambda: True
|
||||||
mod.gateway_is_unlocked = lambda: False
|
mod.gateway_is_unlocked = lambda: True
|
||||||
mod.load_public_auth_file = lambda: {
|
mod.load_public_auth_file = lambda: {
|
||||||
"base_url": "https://api.example.test",
|
"base_url": "https://api.example.test",
|
||||||
"project_id": "project_1",
|
"project_id": "project_1",
|
||||||
@@ -46,10 +47,8 @@ def run():
|
|||||||
}
|
}
|
||||||
mod.write_public_auth = lambda payload: writes.append(dict(payload))
|
mod.write_public_auth = lambda payload: writes.append(dict(payload))
|
||||||
|
|
||||||
def locked(operation="operation"):
|
mod.require_unlocked = lambda operation="operation": None
|
||||||
raise RuntimeError(f"gateway is locked; unlock with the master passphrase before {operation}")
|
mod.write_vault_with_key = lambda key, payload: vault_writes.append((key, payload))
|
||||||
|
|
||||||
mod.require_unlocked = locked
|
|
||||||
|
|
||||||
mod.save_auth({"token": "fresh", "token_expires_at": "new", "worker_id": "worker_1"})
|
mod.save_auth({"token": "fresh", "token_expires_at": "new", "worker_id": "worker_1"})
|
||||||
assert writes[-1]["token"] == "fresh"
|
assert writes[-1]["token"] == "fresh"
|
||||||
@@ -71,14 +70,11 @@ def run():
|
|||||||
assert mod.TOKEN == "newer"
|
assert mod.TOKEN == "newer"
|
||||||
assert mod.TOKEN_EXPIRES_AT == "later"
|
assert mod.TOKEN_EXPIRES_AT == "later"
|
||||||
|
|
||||||
try:
|
mod.save_auth({"gateway_default_command": "bash -l"})
|
||||||
mod.save_auth({"gateway_default_command": "bash -l"})
|
assert vault_writes[-1][0] == b"k" * 32
|
||||||
except RuntimeError as exc:
|
assert vault_writes[-1][1]["secrets"]["gateway_default_command"] == "bash -l"
|
||||||
assert "updating gateway vault" in str(exc)
|
|
||||||
else:
|
|
||||||
raise AssertionError("gateway vault updates must still require unlock")
|
|
||||||
|
|
||||||
print("ok: locked gateway auth refresh smoke passed")
|
print("ok: local gateway auth refresh smoke passed")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Smoke-test local gateway storage uses a machine-local key, not interactive unlock."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def load_module():
|
||||||
|
script_path = pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py"
|
||||||
|
sys.path.insert(0, str(script_path.parent))
|
||||||
|
spec = importlib.util.spec_from_file_location("napseer_mcp_server_local_vault_test", script_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec and spec.loader
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
mod = load_module()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
state = pathlib.Path(tmp)
|
||||||
|
mod.AUTH_DIR = state
|
||||||
|
mod.AUTH_PATH = state / "auth.json"
|
||||||
|
mod.VAULT_PATH = state / "vault.json"
|
||||||
|
mod.LOCAL_VAULT_KEY_PATH = state / "vault.local-key"
|
||||||
|
mod.GATEWAY_RELAY_SECRET_PATH = state / "gateway-relay-secret.json"
|
||||||
|
mod.GATEWAY_RELAY_STATE_PATH = state / "gateway-relay-state.json"
|
||||||
|
mod.CONTAINER_IDENTITY_PATH = state / "container-identity.json"
|
||||||
|
mod.TELEMETRY_PATH = state / "telemetry.json"
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
"base_url": "https://api.example.test",
|
||||||
|
"project_id": "project-1",
|
||||||
|
"token": "token",
|
||||||
|
"token_expires_at": "later",
|
||||||
|
"ssh_key_path": str(state / "id_ed25519"),
|
||||||
|
}
|
||||||
|
mod.AUTH = dict(auth)
|
||||||
|
mod.BASE_URL = auth["base_url"]
|
||||||
|
mod.DEFAULT_PROJECT_ID = auth["project_id"]
|
||||||
|
mod.TOKEN = auth["token"]
|
||||||
|
mod.TOKEN_EXPIRES_AT = auth["token_expires_at"]
|
||||||
|
mod.load_auth = lambda public_override=None, secret_override=None: {
|
||||||
|
**auth,
|
||||||
|
**(public_override or {}),
|
||||||
|
**(secret_override or {}),
|
||||||
|
}
|
||||||
|
mod.ensure_gateway_worker_capability = lambda refresh=True: {"status": "updated"}
|
||||||
|
|
||||||
|
result = mod.gateway_setup({"passphrase": "relay-passphrase", "default_command": "bash -l"})
|
||||||
|
assert result["status"] == "local_available"
|
||||||
|
assert mod.LOCAL_VAULT_KEY_PATH.exists()
|
||||||
|
assert mod.GATEWAY_RELAY_SECRET_PATH.exists()
|
||||||
|
assert mod.read_vault(mod.LOCAL_VAULT_KEY_PATH.read_text(encoding="utf-8").strip())["secrets"]["gateway_default_command"] == "bash -l"
|
||||||
|
|
||||||
|
mod.VAULT_SECRETS = {}
|
||||||
|
mod.VAULT_KEY = None
|
||||||
|
mod.GATEWAY_UNLOCKED = False
|
||||||
|
opened = mod.gateway_open_local()
|
||||||
|
assert opened["status"] == "local_available"
|
||||||
|
assert mod.VAULT_SECRETS["gateway_default_command"] == "bash -l"
|
||||||
|
|
||||||
|
print("ok: local gateway vault key smoke passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -321,8 +321,7 @@ def run():
|
|||||||
mod.vault_exists = lambda: True
|
mod.vault_exists = lambda: True
|
||||||
cache_secret(mod, version=4, active=False)
|
cache_secret(mod, version=4, active=False)
|
||||||
assert mod.MEMORY_SECRET_CACHE
|
assert mod.MEMORY_SECRET_CACHE
|
||||||
locked = mod.gateway_lock()
|
mod.clear_gateway_runtime_caches()
|
||||||
assert locked["status"] == "locked"
|
|
||||||
assert mod.MEMORY_SECRET_CACHE == {}
|
assert mod.MEMORY_SECRET_CACHE == {}
|
||||||
assert mod.ACTIVE_MEMORY_SECRET_VERSIONS == {}
|
assert mod.ACTIVE_MEMORY_SECRET_VERSIONS == {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user