Fix project encryption and gateway vault CLI

This commit is contained in:
HumbleTurtle
2026-05-05 05:49:49 -03:00
parent c7973dac2c
commit 0f981a4b07
5 changed files with 276 additions and 25 deletions
+25 -6
View File
@@ -607,6 +607,23 @@ def service_logs(kind, args):
def handle_gateway(args): 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"}:
print("Usage: nap gateway [start|stop|status|logs|configure|setup|unlock|lock|restart|kill|vault|terminal|schedule|service]")
print(" nap gateway start [--port PORT] Start the local gateway service.")
print(" nap gateway status Show the managed gateway process.")
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 vault Show gateway vault status and pending setup requests.")
print(" nap gateway vault process Complete pending setup requests.")
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway")
return 0
if subcommand in {"vault", "vault-setup", "vault_setup", "project-vault"} and rest and rest[0] in {"help", "-h", "--help"}:
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway] [--all] [--project-id ID]")
print(" status Show configured/locked state and pending setup requests.")
print(" process Generate local secret material and send encrypted envelopes only.")
print(" rotate-secret Rotate one encrypted project secret.")
print(" vault-setup Deprecated alias for vault.")
return 0
if subcommand == "start": if subcommand == "start":
return print_json(start_service("gateway", ["--no-browser", *rest])) or 0 return print_json(start_service("gateway", ["--no-browser", *rest])) or 0
if subcommand == "stop": if subcommand == "stop":
@@ -617,9 +634,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 in {"configure", "config"}: if subcommand in {"configure", "config"}:
return run_wrapper("gateway", ["configure", *rest]) return run_wrapper("gateway", ["configure", *rest])
if subcommand in {"setup", "unlock", "lock", "restart", "kill"}: if subcommand in {"setup", "unlock", "lock", "restart", "kill", "vault", "vault-setup", "vault_setup", "project-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") raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|unlock|lock|restart|kill|logs|status|vault|terminal|schedule|service")
def print_json(value): def print_json(value):
@@ -680,11 +697,13 @@ def main(argv):
print("Usage: nap [auth|project|gateway|agent|feedback|status|update|where|install]") print("Usage: nap [auth|project|gateway|agent|feedback|status|update|where|install]")
print(" auth Auth commands: login-local, status.") print(" auth Auth commands: login-local, status.")
print(" Example: nap auth login-local") print(" Example: nap auth login-local")
print(" project Project commands: create, status, encryption, plaintext, encrypted.") print(" project Project commands: create, status, encryption, encrypted, plaintext.")
print(" Examples: nap project create, nap project status, nap project encryption set plaintext") print(" Examples: nap project create --encrypted, nap project encrypted, nap project plaintext")
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, restart, kill, logs, status.") print(" Project encryption uses a project passphrase; use --passphrase or NAPSEER_PROJECT_PASSPHRASE for automation.")
print(" Equivalent explicit form: nap project encryption set encrypted|plaintext")
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, 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 start --port 8767, nap gateway logs") 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.")
print(" list shows registered project agents; workspaces shows /agents folders.") print(" list shows registered project agents; workspaces shows /agents folders.")
print(" feedback Feedback commands: list, global, status, resolve.") print(" feedback Feedback commands: list, global, status, resolve.")
+120 -12
View File
@@ -477,6 +477,7 @@ GATEWAY_SCHEDULES_SCHEMA = "napseer.gateway-schedules.v1"
CONTAINER_IDENTITY_SCHEMA = "napseer.container-identity.v1" CONTAINER_IDENTITY_SCHEMA = "napseer.container-identity.v1"
GATEWAY_RELAY_SECRET_ITERATIONS = int(os.environ.get("NAPSEER_GATEWAY_RELAY_SECRET_ITERATIONS", "210000")) GATEWAY_RELAY_SECRET_ITERATIONS = int(os.environ.get("NAPSEER_GATEWAY_RELAY_SECRET_ITERATIONS", "210000"))
VAULT_KDF_ITERATIONS = int(os.environ.get("NAPSEER_VAULT_KDF_ITERATIONS", "600000")) VAULT_KDF_ITERATIONS = int(os.environ.get("NAPSEER_VAULT_KDF_ITERATIONS", "600000"))
PROJECT_KEY_KDF_ITERATIONS = int(os.environ.get("NAPSEER_PROJECT_KEY_KDF_ITERATIONS", "600000"))
GATEWAY_IDLE_TIMEOUT_SECONDS = int(os.environ.get("NAPSEER_GATEWAY_IDLE_TIMEOUT_SECONDS", "900")) GATEWAY_IDLE_TIMEOUT_SECONDS = int(os.environ.get("NAPSEER_GATEWAY_IDLE_TIMEOUT_SECONDS", "900"))
GATEWAY_MAX_LOG_BYTES = int(os.environ.get("NAPSEER_GATEWAY_MAX_LOG_BYTES", str(5 * 1024 * 1024))) GATEWAY_MAX_LOG_BYTES = int(os.environ.get("NAPSEER_GATEWAY_MAX_LOG_BYTES", str(5 * 1024 * 1024)))
GATEWAY_PROJECT_SECRET_KINDS = ("chat", "tabs", "gateway") GATEWAY_PROJECT_SECRET_KINDS = ("chat", "tabs", "gateway")
@@ -682,6 +683,23 @@ def read_master_passphrase(create=False):
return first return first
def read_project_passphrase(create=False):
value = os.environ.get("NAPSEER_PROJECT_PASSPHRASE") or os.environ.get("NAPSEER_MASTER_PASSPHRASE")
if value:
return value
if not sys.stdin.isatty():
raise RuntimeError(
"Project encryption needs a local wrapping passphrase. "
"Pass --passphrase TEXT or set NAPSEER_PROJECT_PASSPHRASE."
)
first = getpass.getpass("Create project encryption passphrase: " if create else "Project encryption passphrase: ")
if create:
second = getpass.getpass("Confirm project encryption passphrase: ")
if first != second:
raise RuntimeError("project encryption passphrases did not match")
return first
def vault_exists(): def vault_exists():
return VAULT_PATH.exists() return VAULT_PATH.exists()
@@ -3042,6 +3060,46 @@ def gateway_vault_master_wrap_aad(project_id, credential_id):
return f"napseer.vault-master-wrap.v1:{project_id}:{credential_id}".encode("utf-8") return f"napseer.vault-master-wrap.v1:{project_id}:{credential_id}".encode("utf-8")
def project_key_wrap_aad(project_id, credential_id):
return f"napseer.project-key-wrap.v1:{project_id}:{credential_id}".encode("utf-8")
def local_wrapped_project_key(project_id, passphrase=None, project_key=None):
project_id = str(project_id or current_project_id()).strip()
if not project_id:
raise RuntimeError("project_id is required")
passphrase = passphrase or read_project_passphrase(create=True)
if not isinstance(passphrase, str) or not passphrase:
raise RuntimeError("project encryption passphrase is required")
project_key = project_key or secrets.token_bytes(32)
credential_id = f"local-passphrase:{AUTH.get('agent_id') or 'local'}"
salt = secrets.token_bytes(16)
nonce = secrets.token_bytes(12)
wrapping_key = hashlib.pbkdf2_hmac(
"sha256",
passphrase.encode("utf-8"),
salt,
PROJECT_KEY_KDF_ITERATIONS,
dklen=32,
)
aad = project_key_wrap_aad(project_id, credential_id)
packed = aes_gcm_encrypt(wrapping_key, nonce, aad, project_key)
return {
"credential_id": credential_id,
"wrapped_project_key_b64": base64.b64encode(packed).decode("ascii"),
"wrap_alg": "AES-GCM-256",
"kdf_alg": "PBKDF2-SHA256",
"salt_b64": base64.b64encode(salt).decode("ascii"),
"kdf_params": {
"source": "local_passphrase",
"iterations": PROJECT_KEY_KDF_ITERATIONS,
"aad_hash": hashlib.sha256(aad).hexdigest(),
"nonce_b64": base64.b64encode(nonce).decode("ascii"),
"runtime": "nap-cli",
},
}
def gateway_project_secret_envelope(project_id, secret_kind, version, vault_master_secret, secret_material): def gateway_project_secret_envelope(project_id, secret_kind, version, vault_master_secret, secret_material):
nonce = secrets.token_bytes(12) nonce = secrets.token_bytes(12)
aad = gateway_project_secret_aad(project_id, secret_kind, version) aad = gateway_project_secret_aad(project_id, secret_kind, version)
@@ -3138,6 +3196,27 @@ def gateway_vault_setup_requests(args=None):
return request_json("GET", f"/v1/projects/{project_id}/vault/setup-requests") return request_json("GET", f"/v1/projects/{project_id}/vault/setup-requests")
def gateway_vault_status(args=None):
args = args or {}
project_id = args.get("project_id") or current_project_id()
requests = gateway_vault_setup_requests({"project_id": project_id})
items = requests.get("items", [])
pending = [item for item in items if item.get("status") == "pending"]
return {
"status": "ok",
"project_id": project_id,
"locked": not gateway_is_unlocked(),
"vault_configured": vault_exists(),
"pending_setup_requests": len(pending),
"items": items,
"message": (
"No pending gateway vault setup requests."
if not pending
else f"{len(pending)} pending gateway vault setup request(s). Run `nap gateway vault process` to complete them."
),
}
def gateway_complete_vault_setup_request(setup_request, project_id=None): def gateway_complete_vault_setup_request(setup_request, project_id=None):
require_unlocked("completing project vault setup") require_unlocked("completing project vault setup")
project_id = str(project_id or setup_request.get("project_id") or current_project_id()) project_id = str(project_id or setup_request.get("project_id") or current_project_id())
@@ -3164,12 +3243,22 @@ def gateway_complete_vault_setup_request(setup_request, project_id=None):
def gateway_process_vault_setup_requests(args=None): def gateway_process_vault_setup_requests(args=None):
args = args or {} args = args or {}
require_unlocked("processing project vault setup requests")
project_id = args.get("project_id") or current_project_id() project_id = args.get("project_id") or current_project_id()
items = gateway_vault_setup_requests({"project_id": project_id}).get("items", []) items = gateway_vault_setup_requests({"project_id": project_id}).get("items", [])
pending = [item for item in items if item.get("status") == "pending"]
if not pending:
return {
"status": "noop",
"project_id": project_id,
"completed_count": 0,
"completed": [],
"skipped": [{"id": item.get("id"), "status": item.get("status")} for item in items if item.get("status") != "pending"],
"message": "No pending gateway vault setup requests.",
}
require_unlocked("processing project vault setup requests")
completed = [] completed = []
skipped = [] skipped = []
for item in items: for item in pending:
if item.get("status") != "pending": if item.get("status") != "pending":
skipped.append({"id": item.get("id"), "status": item.get("status")}) skipped.append({"id": item.get("id"), "status": item.get("status")})
continue continue
@@ -5863,10 +5952,11 @@ def bootstrap_project(args):
project, project_status, message = create_project_with_state(args) project, project_status, message = create_project_with_state(args)
encryption_state = "plaintext" encryption_state = "plaintext"
if encryption == "encrypted": if encryption == "encrypted":
wrapped_project_key = local_wrapped_project_key(project["id"], passphrase=args.get("passphrase"))
status = request_project_write( status = request_project_write(
"POST", "POST",
f"/v1/projects/{project['id']}/encryption/enable", f"/v1/projects/{project['id']}/encryption/enable",
{"state": "encrypted"}, {"state": "encrypted", "wrapped_project_key": wrapped_project_key},
project["id"], project["id"],
"enable project encryption", "enable project encryption",
) )
@@ -5915,6 +6005,8 @@ def project_encryption_transition(args=None):
project_id = resolve_project_id(args) project_id = resolve_project_id(args)
operation = "enable" if desired == "encrypted" else "disable" operation = "enable" if desired == "encrypted" else "disable"
payload = {"state": desired} payload = {"state": desired}
if desired == "encrypted":
payload["wrapped_project_key"] = local_wrapped_project_key(project_id, passphrase=args.get("passphrase"))
result = request_project_write( result = request_project_write(
"POST", "POST",
f"/v1/projects/{project_id}/encryption/{operation}", f"/v1/projects/{project_id}/encryption/{operation}",
@@ -9666,17 +9758,26 @@ def cli_main(argv):
if action == "set": if action == "set":
if len(argv) <= 4: if len(argv) <= 4:
raise RuntimeError("nap project encryption set requires plaintext or encrypted") raise RuntimeError("nap project encryption set requires plaintext or encrypted")
print(json.dumps(project_encryption_transition({"state": argv[4]}), indent=2)) print(json.dumps(project_encryption_transition({
"state": argv[4],
"passphrase": cli_option(argv[5:], "--passphrase", default=None),
}), indent=2))
return return
print(json.dumps(project_encryption_transition({"state": action}), indent=2)) print(json.dumps(project_encryption_transition({
"state": action,
"passphrase": cli_option(argv[4:], "--passphrase", default=None),
}), indent=2))
return return
if subcommand in {"plaintext", "plain", "unencrypt", "decrypt", "disable-encryption"}: if subcommand in {"plaintext", "plain", "unencrypt", "decrypt", "disable-encryption"}:
print(json.dumps(project_encryption_transition({"state": "plaintext"}), indent=2)) print(json.dumps(project_encryption_transition({"state": "plaintext"}), indent=2))
return return
if subcommand in {"encrypted", "encrypt", "enable-encryption"}: if subcommand in {"encrypted", "encrypt", "enable-encryption"}:
print(json.dumps(project_encryption_transition({"state": "encrypted"}), indent=2)) print(json.dumps(project_encryption_transition({
"state": "encrypted",
"passphrase": cli_option(argv[3:], "--passphrase", default=None),
}), indent=2))
return return
print("Usage: napseer_mcp_server.py project [create [slug] [--name NAME] [--description TEXT] [--encrypted|--encryption plaintext|encrypted] [--passphrase TEXT]|claim [--no-browser] [--timeout SECONDS] [--return-url URL]|status|encryption [status|set plaintext|set encrypted|plaintext|encrypted]|plaintext|encrypted]") print("Usage: napseer_mcp_server.py project [create [slug] [--name NAME] [--description TEXT] [--encrypted|--encryption plaintext|encrypted] [--passphrase TEXT]|claim [--no-browser] [--timeout SECONDS] [--return-url URL]|status|encryption [status|set plaintext|set encrypted|plaintext|encrypted] [--passphrase TEXT]|plaintext|encrypted [--passphrase TEXT]]")
return return
if command == "gateway": if command == "gateway":
subcommand = argv[2] if len(argv) > 2 else "status" subcommand = argv[2] if len(argv) > 2 else "status"
@@ -9809,16 +9910,19 @@ def cli_main(argv):
gateway_service_run(payload) gateway_service_run(payload)
return return
raise RuntimeError("unknown gateway service action; use preregister|activate|run") raise RuntimeError("unknown gateway service action; use preregister|activate|run")
if subcommand in {"vault-setup", "vault_setup", "project-vault"}: if subcommand in {"vault", "vault-setup", "vault_setup", "project-vault"}:
action = args[0] if args else "process" action = args[0] if args else "status"
vault_args = args[1:] if args else [] vault_args = args[1:] if args else []
if action in {"help", "-h", "--help"}:
print("Usage: napseer_mcp_server.py gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway] [--all] [--project-id ID]")
return
payload = { payload = {
"project_id": cli_option(vault_args, "--project-id", default=None), "project_id": cli_option(vault_args, "--project-id", default=None),
"secret_kind": cli_option(vault_args, "--secret-kind", "--kind", default=None), "secret_kind": cli_option(vault_args, "--secret-kind", "--kind", default=None),
"complete_all": cli_flag(vault_args, "--all", "--complete-all"), "complete_all": cli_flag(vault_args, "--all", "--complete-all"),
} }
if action in {"list", "ls", "status"}: if action in {"list", "ls", "status"}:
print(json.dumps(gateway_vault_setup_requests(payload), indent=2)) print(json.dumps(gateway_vault_status(payload), indent=2))
return return
if action in {"process", "complete", "run"}: if action in {"process", "complete", "run"}:
print(json.dumps(gateway_process_vault_setup_requests(payload), indent=2)) print(json.dumps(gateway_process_vault_setup_requests(payload), indent=2))
@@ -9828,7 +9932,7 @@ def cli_main(argv):
payload["secret_kind"] = vault_args[0] if vault_args and not vault_args[0].startswith("-") else "chat" payload["secret_kind"] = vault_args[0] if vault_args and not vault_args[0].startswith("-") else "chat"
print(json.dumps(gateway_rotate_project_vault_secret(payload), indent=2)) print(json.dumps(gateway_rotate_project_vault_secret(payload), indent=2))
return return
raise RuntimeError("unknown gateway vault-setup action; use list|process") raise RuntimeError("unknown gateway vault action; use status|list|process|rotate-secret")
if subcommand == "setup": if subcommand == "setup":
print(json.dumps(gateway_setup({ print(json.dumps(gateway_setup({
"passphrase": cli_option(args, "--passphrase", default=None), "passphrase": cli_option(args, "--passphrase", default=None),
@@ -9846,7 +9950,7 @@ def cli_main(argv):
if subcommand == "lock": if subcommand == "lock":
print(json.dumps(gateway_lock(), indent=2)) print(json.dumps(gateway_lock(), indent=2))
return return
print("Usage: napseer_mcp_server.py gateway [status|configure [--command CMD] [--target session:window.pane]|vault-setup [list|process] [--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]") 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] [--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"
@@ -9943,4 +10047,8 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
try:
cli_main(sys.argv) cli_main(sys.argv)
except Exception as exc:
print(json.dumps({"status": "error", "error": str(exc)}, indent=2), file=sys.stderr)
sys.exit(1)
+25 -6
View File
@@ -607,6 +607,23 @@ def service_logs(kind, args):
def handle_gateway(args): 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"}:
print("Usage: nap gateway [start|stop|status|logs|configure|setup|unlock|lock|restart|kill|vault|terminal|schedule|service]")
print(" nap gateway start [--port PORT] Start the local gateway service.")
print(" nap gateway status Show the managed gateway process.")
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 vault Show gateway vault status and pending setup requests.")
print(" nap gateway vault process Complete pending setup requests.")
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway")
return 0
if subcommand in {"vault", "vault-setup", "vault_setup", "project-vault"} and rest and rest[0] in {"help", "-h", "--help"}:
print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind chat|tabs|gateway] [--all] [--project-id ID]")
print(" status Show configured/locked state and pending setup requests.")
print(" process Generate local secret material and send encrypted envelopes only.")
print(" rotate-secret Rotate one encrypted project secret.")
print(" vault-setup Deprecated alias for vault.")
return 0
if subcommand == "start": if subcommand == "start":
return print_json(start_service("gateway", ["--no-browser", *rest])) or 0 return print_json(start_service("gateway", ["--no-browser", *rest])) or 0
if subcommand == "stop": if subcommand == "stop":
@@ -617,9 +634,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 in {"configure", "config"}: if subcommand in {"configure", "config"}:
return run_wrapper("gateway", ["configure", *rest]) return run_wrapper("gateway", ["configure", *rest])
if subcommand in {"setup", "unlock", "lock", "restart", "kill"}: if subcommand in {"setup", "unlock", "lock", "restart", "kill", "vault", "vault-setup", "vault_setup", "project-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") raise RuntimeError("unknown gateway command; use nap gateway start|stop|configure|setup|unlock|lock|restart|kill|logs|status|vault|terminal|schedule|service")
def print_json(value): def print_json(value):
@@ -680,11 +697,13 @@ def main(argv):
print("Usage: nap [auth|project|gateway|agent|feedback|status|update|where|install]") print("Usage: nap [auth|project|gateway|agent|feedback|status|update|where|install]")
print(" auth Auth commands: login-local, status.") print(" auth Auth commands: login-local, status.")
print(" Example: nap auth login-local") print(" Example: nap auth login-local")
print(" project Project commands: create, status, encryption, plaintext, encrypted.") print(" project Project commands: create, status, encryption, encrypted, plaintext.")
print(" Examples: nap project create, nap project status, nap project encryption set plaintext") print(" Examples: nap project create --encrypted, nap project encrypted, nap project plaintext")
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, restart, kill, logs, status.") print(" Project encryption uses a project passphrase; use --passphrase or NAPSEER_PROJECT_PASSPHRASE for automation.")
print(" Equivalent explicit form: nap project encryption set encrypted|plaintext")
print(" gateway Gateway commands: start, stop, configure, setup, unlock, lock, 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 start --port 8767, nap gateway logs") 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.")
print(" list shows registered project agents; workspaces shows /agents folders.") print(" list shows registered project agents; workspaces shows /agents folders.")
print(" feedback Feedback commands: list, global, status, resolve.") print(" feedback Feedback commands: list, global, status, resolve.")
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Smoke-test top-level nap gateway command routing."""
import importlib.util
import pathlib
import sys
def load_module():
script_path = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "nap_install.py"
spec = importlib.util.spec_from_file_location("nap_install", 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()
calls = []
mod.run_wrapper = lambda command, args: calls.append((command, args)) or 0
assert mod.handle_gateway(["--help"]) == 0
assert calls == []
assert mod.handle_gateway(["vault", "--help"]) == 0
assert calls == []
assert mod.handle_gateway(["vault", "list"]) == 0
assert calls[-1] == ("gateway", ["vault", "list"])
assert mod.handle_gateway(["vault", "rotate-secret", "--kind", "chat"]) == 0
assert calls[-1] == ("gateway", ["vault", "rotate-secret", "--kind", "chat"])
assert mod.handle_gateway(["vault-setup", "list"]) == 0
assert calls[-1] == ("gateway", ["vault-setup", "list"])
assert mod.handle_gateway(["terminal", "list"]) == 0
assert calls[-1] == ("gateway", ["terminal", "list"])
print("ok: nap installer gateway route smoke passed")
if __name__ == "__main__":
run()
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Smoke-test project encryption enable payload generation."""
import base64
import importlib.util
import pathlib
import sys
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", 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()
calls = []
mod.DEFAULT_PROJECT_ID = "project-1"
mod.AUTH = {"agent_id": "agent-1"}
mod.request_project_write = lambda method, path, body, project_id, purpose, scope_type="project": calls.append(
(method, path, body, project_id, purpose, scope_type)
) or {"state": "encrypted", "active_project_key_version": {"version": 1}}
mod.save_auth = lambda payload: None
result = mod.project_encryption_transition({"state": "encrypted", "passphrase": "pw"})
assert result["status"] == "updated"
assert calls, "encryption enable should call backend"
method, path, body, project_id, purpose, scope_type = calls[0]
assert method == "POST"
assert path == "/v1/projects/project-1/encryption/enable"
assert project_id == "project-1"
assert scope_type == "encryption"
assert body["state"] == "encrypted"
wrapped = body["wrapped_project_key"]
assert wrapped["credential_id"] == "local-passphrase:agent-1"
assert wrapped["wrap_alg"] == "AES-GCM-256"
assert wrapped["kdf_alg"] == "PBKDF2-SHA256"
assert wrapped["kdf_params"]["source"] == "local_passphrase"
assert wrapped["kdf_params"]["iterations"] == mod.PROJECT_KEY_KDF_ITERATIONS
assert len(base64.b64decode(wrapped["salt_b64"])) == 16
assert len(base64.b64decode(wrapped["kdf_params"]["nonce_b64"])) == 12
assert len(base64.b64decode(wrapped["wrapped_project_key_b64"])) == 48
calls.clear()
result = mod.project_encryption_transition({"state": "plaintext"})
assert result["status"] == "updated"
assert "wrapped_project_key" not in calls[0][2]
print("ok: project encryption enable payload smoke passed")
if __name__ == "__main__":
run()