Separate vault and gateway passphrases

This commit is contained in:
HumbleTurtle
2026-05-20 00:27:43 -03:00
parent e24daf54c1
commit 18c67ef27d
8 changed files with 144 additions and 41 deletions
+5 -5
View File
@@ -597,16 +597,16 @@ def handle_gateway(args):
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 migrate local gateway storage.") 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 with the vault/content passphrase.")
print(" nap gateway vault rotate-secret --kind memory") print(" nap gateway vault rotate-secret --kind memory")
print(" nap chat secret setup Configure chat secret state through the gateway.") print(" nap chat secret setup Configure chat secret state with the vault/content passphrase.")
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 memory] [--all] [--project-id ID]") print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind memory] [--all] [--project-id ID] [--vault-passphrase TEXT]")
print(" status Show local gateway 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 the memory project secret from the gateway worker path.") print(" rotate-secret Rotate the memory project secret from the gateway worker path.")
print(" Chat secrets use: nap chat secret setup|status|rotate") print(" Chat secrets use: nap chat secret setup|status|rotate; setup uses the same vault/content passphrase.")
return 0 return 0
if subcommand == "process": if subcommand == "process":
return run_wrapper("gateway", ["vault", "process", *rest]) return run_wrapper("gateway", ["vault", "process", *rest])
@@ -687,7 +687,7 @@ def main(argv):
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 the local gateway: nap gateway vault process") print(" Encrypted project setup is completed by the local gateway: nap gateway vault process")
print(" chat Chat secret commands: secret status, secret setup, secret rotate.") print(" chat Chat secret commands: secret status, secret setup, secret rotate.")
print(" Examples: nap chat secret setup, nap chat secret status") print(" Examples: nap chat secret setup --vault-passphrase TEXT, nap chat secret status")
print(" plan Plan commands: list, to-kanban.") print(" plan Plan commands: list, to-kanban.")
print(" Example: nap plan to-kanban /plans/example --column todo") print(" Example: nap plan to-kanban /plans/example --column todo")
print(" lineage Generic source-record -> plan -> kanban lineage checks.") print(" lineage Generic source-record -> plan -> kanban lineage checks.")
+55 -20
View File
@@ -531,7 +531,7 @@ MEMORY_NODE_HKDF_INFO_PREFIX = "napseer-memory-node-v1"
MEMORY_NODE_LEGACY_HKDF_INFO_PREFIX = "napseer-memory-local-mcp-v1" MEMORY_NODE_LEGACY_HKDF_INFO_PREFIX = "napseer-memory-local-mcp-v1"
MEMORY_SECRET_CACHE = {} MEMORY_SECRET_CACHE = {}
ACTIVE_MEMORY_SECRET_VERSIONS = {} ACTIVE_MEMORY_SECRET_VERSIONS = {}
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = None PROJECT_VAULT_PASSPHRASE = None
VAULT_SECRETS = {} VAULT_SECRETS = {}
VAULT_KEY = None VAULT_KEY = None
@@ -720,12 +720,12 @@ def _derive_vault_key(passphrase, salt, iterations):
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)
def read_master_passphrase(create=False): def read_gateway_relay_passphrase(create=False):
value = os.environ.get("NAPSEER_MASTER_PASSPHRASE") value = os.environ.get("NAPSEER_GATEWAY_PASSPHRASE")
if value: if value:
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_GATEWAY_PASSPHRASE is required in non-interactive mode")
first = getpass.getpass("Create gateway relay passphrase: " if create else "Gateway relay passphrase: ") first = getpass.getpass("Create gateway relay passphrase: " if create else "Gateway relay passphrase: ")
if create: if create:
second = getpass.getpass("Confirm gateway relay passphrase: ") second = getpass.getpass("Confirm gateway relay passphrase: ")
@@ -734,6 +734,20 @@ def read_master_passphrase(create=False):
return first return first
def read_project_vault_passphrase(create=False):
value = os.environ.get("NAPSEER_VAULT_PASSPHRASE") or os.environ.get("NAPSEER_MASTER_PASSPHRASE")
if value:
return value
if not sys.stdin.isatty():
raise RuntimeError("NAPSEER_VAULT_PASSPHRASE is required in non-interactive mode")
first = getpass.getpass("Create project vault passphrase: " if create else "Project vault passphrase: ")
if create:
second = getpass.getpass("Confirm project vault passphrase: ")
if first != second:
raise RuntimeError("project vault passphrases did not match")
return first
def read_local_vault_key(create=False): def read_local_vault_key(create=False):
value = os.environ.get("NAPSEER_LOCAL_VAULT_KEY") value = os.environ.get("NAPSEER_LOCAL_VAULT_KEY")
if value: if value:
@@ -1098,8 +1112,8 @@ def touch_gateway():
def clear_gateway_runtime_caches(): def clear_gateway_runtime_caches():
global GATEWAY_PROJECT_BUNDLE_PASSPHRASE global PROJECT_VAULT_PASSPHRASE
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = None PROJECT_VAULT_PASSPHRASE = None
MEMORY_SECRET_CACHE.clear() MEMORY_SECRET_CACHE.clear()
ACTIVE_MEMORY_SECRET_VERSIONS.clear() ACTIVE_MEMORY_SECRET_VERSIONS.clear()
@@ -1222,7 +1236,7 @@ def apply_vault_secrets(secrets_payload):
def gateway_open_local(passphrase=None): 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
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) local_key = read_local_vault_key(create=False)
@@ -1286,7 +1300,6 @@ def gateway_open_local(passphrase=None):
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"]
if passphrase: if passphrase:
GATEWAY_PROJECT_BUNDLE_PASSPHRASE = str(passphrase)
write_gateway_relay_secret(passphrase) write_gateway_relay_secret(passphrase)
persist_vault_secrets() persist_vault_secrets()
touch_gateway() touch_gateway()
@@ -1333,7 +1346,7 @@ def normalize_gateway_command(value=None):
def gateway_setup(args): def gateway_setup(args):
passphrase = args.get("passphrase") or os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or read_master_passphrase(create=True) passphrase = args.get("passphrase") or os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or read_gateway_relay_passphrase(create=True)
if vault_exists() and not args.get("overwrite"): if vault_exists() and not args.get("overwrite"):
result = gateway_open_local(passphrase) result = gateway_open_local(passphrase)
capability_result = ensure_gateway_worker_capability(refresh=True) capability_result = ensure_gateway_worker_capability(refresh=True)
@@ -1388,7 +1401,6 @@ def gateway_rotate_passphrase(args):
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 {
@@ -3156,13 +3168,23 @@ def current_account_id():
return account_id return account_id
def set_project_vault_passphrase(passphrase):
global PROJECT_VAULT_PASSPHRASE
if not isinstance(passphrase, str) or not passphrase.strip():
raise RuntimeError("project vault passphrase is required")
PROJECT_VAULT_PASSPHRASE = passphrase.strip()
return PROJECT_VAULT_PASSPHRASE
def project_bundle_passphrase(): def project_bundle_passphrase():
if GATEWAY_PROJECT_BUNDLE_PASSPHRASE: if PROJECT_VAULT_PASSPHRASE:
return GATEWAY_PROJECT_BUNDLE_PASSPHRASE return PROJECT_VAULT_PASSPHRASE
value = os.environ.get("NAPSEER_MASTER_PASSPHRASE") value = os.environ.get("NAPSEER_VAULT_PASSPHRASE") or os.environ.get("NAPSEER_MASTER_PASSPHRASE")
if value: if value:
return value return value
raise RuntimeError("gateway relay passphrase is required before handling project key bundles") if sys.stdin.isatty():
return set_project_vault_passphrase(read_project_vault_passphrase(create=False))
raise RuntimeError("project vault passphrase is required before handling project key bundles; set NAPSEER_VAULT_PASSPHRASE")
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):
@@ -3752,6 +3774,7 @@ def gateway_generate_project_vault_setup_payload(project_id, setup_request_id):
project_id = str(project_id or current_project_id()).strip() project_id = str(project_id or current_project_id()).strip()
if not project_id: if not project_id:
raise RuntimeError("project_id is required") raise RuntimeError("project_id is required")
project_bundle_passphrase()
account_id = current_account_id() account_id = current_account_id()
plaintext_bundle = generate_project_data_key_bundle(project_id, account_id, data_key_epoch=1) plaintext_bundle = generate_project_data_key_bundle(project_id, account_id, data_key_epoch=1)
project_secrets = [] project_secrets = []
@@ -3826,6 +3849,8 @@ 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 {}
if args.get("passphrase") or args.get("vault_passphrase"):
set_project_vault_passphrase(args.get("passphrase") or args.get("vault_passphrase"))
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"] pending = [item for item in items if item.get("status") == "pending"]
@@ -3951,6 +3976,7 @@ def chat_secret_status(args=None):
response = { response = {
"status": "ok", "status": "ok",
"project_id": project_id, "project_id": project_id,
"passphrase_authority": "vault",
"locked": vault.get("locked"), "locked": vault.get("locked"),
"vault_configured": vault.get("vault_configured"), "vault_configured": vault.get("vault_configured"),
"pending_setup_requests": vault.get("pending_setup_requests", 0), "pending_setup_requests": vault.get("pending_setup_requests", 0),
@@ -4008,6 +4034,8 @@ def chat_secret_status(args=None):
def chat_secret_setup(args=None): def chat_secret_setup(args=None):
args = args or {} args = args or {}
if args.get("passphrase") or args.get("vault_passphrase"):
set_project_vault_passphrase(args.get("passphrase") or args.get("vault_passphrase"))
project_id = str(args.get("project_id") or current_project_id()).strip() project_id = str(args.get("project_id") or current_project_id()).strip()
status = chat_secret_status({"project_id": project_id}) status = chat_secret_status({"project_id": project_id})
if status.get("chat_secret_configured") is True: if status.get("chat_secret_configured") is True:
@@ -4033,10 +4061,11 @@ def chat_secret_rotate(args=None):
return { return {
"status": "operator_auth_required", "status": "operator_auth_required",
"project_id": project_id, "project_id": project_id,
"passphrase_authority": "vault",
"message": ( "message": (
"Chat secret rotation is not available through the gateway worker token. " "Chat secret rotation is not available through the gateway worker token. "
"This preserves the boundary that worker tokens cannot read active chat secrets. " "This preserves the boundary that worker tokens cannot read active chat secrets. "
"Use an operator-authorized rotation flow." "Use an operator-authorized rotation flow with the project vault passphrase."
), ),
} }
@@ -7026,6 +7055,7 @@ def cli_positionals(args):
"--image-ref", "--image-ref",
"--secret-kind", "--secret-kind",
"--kind", "--kind",
"--vault-passphrase",
"--root-path", "--root-path",
"--name", "--name",
"--target", "--target",
@@ -10328,12 +10358,12 @@ def raw_tools():
}, },
{ {
"name": "nap_gateway_setup", "name": "nap_gateway_setup",
"description": "Create or migrate local gateway storage and configure the remote relay passphrase.", "description": "Create or migrate local gateway storage and configure the remote relay passphrase. This does not configure the project vault/content passphrase used for chat or memory.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"required": ["passphrase"], "required": ["passphrase"],
"properties": { "properties": {
"passphrase": {"type": "string", "description": "Remote relay passphrase. Local access is protected by the machine/user session, not an interactive unlock gate."}, "passphrase": {"type": "string", "description": "Gateway relay passphrase for remote terminal/relay access only. It is not the chat/vault passphrase."},
"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"},
}, },
@@ -10405,10 +10435,13 @@ def raw_tools():
}, },
{ {
"name": "nap_chat_secret_setup", "name": "nap_chat_secret_setup",
"description": "Complete pending gateway vault setup needed for chat secrets, or report the next required action.", "description": "Complete pending gateway vault setup needed for chat secrets using the project vault/content passphrase, or report the next required action.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": {"project_id": {"type": "string"}}, "properties": {
"project_id": {"type": "string"},
"vault_passphrase": {"type": "string"},
},
"additionalProperties": False, "additionalProperties": False,
}, },
}, },
@@ -12249,6 +12282,7 @@ def cli_main(argv):
"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"),
"vault_passphrase": cli_option(vault_args, "--vault-passphrase", "--passphrase", default=None),
} }
if action in {"list", "ls", "status"}: if action in {"list", "ls", "status"}:
print(json.dumps(gateway_vault_status(payload), indent=2)) print(json.dumps(gateway_vault_status(payload), indent=2))
@@ -12286,6 +12320,7 @@ def cli_main(argv):
secret_args = args secret_args = args
payload = { payload = {
"project_id": cli_option(secret_args, "--project-id", default=None), "project_id": cli_option(secret_args, "--project-id", default=None),
"vault_passphrase": cli_option(secret_args, "--vault-passphrase", "--passphrase", default=None),
} }
if action in {"status", "show", "ls", "list"}: if action in {"status", "show", "ls", "list"}:
print(json.dumps(chat_secret_status(payload), indent=2)) print(json.dumps(chat_secret_status(payload), indent=2))
@@ -12296,7 +12331,7 @@ def cli_main(argv):
if action in {"rotate", "rotate-secret"}: if action in {"rotate", "rotate-secret"}:
print(json.dumps(chat_secret_rotate(payload), indent=2)) print(json.dumps(chat_secret_rotate(payload), indent=2))
return return
print("Usage: napseer_mcp_server.py chat secret [status|setup|rotate] [--project-id ID]") print("Usage: napseer_mcp_server.py chat secret [status|setup|rotate] [--project-id ID] [--vault-passphrase TEXT]")
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"
+8 -8
View File
@@ -117,16 +117,16 @@ def ensure_local_files():
chmod_best_effort(pathlib.Path(str(KEY_PATH) + ".pub"), 0o644) chmod_best_effort(pathlib.Path(str(KEY_PATH) + ".pub"), 0o644)
def read_master_passphrase(): def read_gateway_passphrase():
value = os.environ.get("NAPSEER_MASTER_PASSPHRASE") value = os.environ.get("NAPSEER_GATEWAY_PASSPHRASE") or os.environ.get("NAPSEER_MASTER_PASSPHRASE")
if value: if value:
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_GATEWAY_PASSPHRASE is required in non-interactive mode")
first = getpass.getpass("Create gateway master passphrase: ") first = getpass.getpass("Create gateway relay/storage passphrase: ")
second = getpass.getpass("Confirm gateway master passphrase: ") second = getpass.getpass("Confirm gateway relay/storage passphrase: ")
if first != second: if first != second:
raise RuntimeError("gateway master passphrases did not match") raise RuntimeError("gateway relay/storage passphrases did not match")
return first return first
@@ -278,7 +278,7 @@ def wait_for_claim_result(receiver, timeout_seconds):
def main(): def main():
ensure_local_files() ensure_local_files()
master_passphrase = read_master_passphrase() gateway_passphrase = read_gateway_passphrase()
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()
private_key = KEY_PATH.read_text(encoding="utf-8") private_key = KEY_PATH.read_text(encoding="utf-8")
project_slug = os.environ.get("NAPSEER_PROJECT_SLUG", default_slug()) project_slug = os.environ.get("NAPSEER_PROJECT_SLUG", default_slug())
@@ -357,7 +357,7 @@ def main():
if account_claim: if account_claim:
secret_auth["account_claim_url"] = account_claim["claim_url"] secret_auth["account_claim_url"] = account_claim["claim_url"]
secret_auth["account_claim_expires_at"] = account_claim["expires_at"] secret_auth["account_claim_expires_at"] = account_claim["expires_at"]
write_gateway_vault(master_passphrase, public_auth, secret_auth) write_gateway_vault(gateway_passphrase, public_auth, secret_auth)
remove_plain_private_key() remove_plain_private_key()
output = { output = {
+5 -5
View File
@@ -597,16 +597,16 @@ def handle_gateway(args):
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 migrate local gateway storage.") 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 with the vault/content passphrase.")
print(" nap gateway vault rotate-secret --kind memory") print(" nap gateway vault rotate-secret --kind memory")
print(" nap chat secret setup Configure chat secret state through the gateway.") print(" nap chat secret setup Configure chat secret state with the vault/content passphrase.")
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 memory] [--all] [--project-id ID]") print("Usage: nap gateway vault [status|list|process|rotate-secret] [--kind memory] [--all] [--project-id ID] [--vault-passphrase TEXT]")
print(" status Show local gateway 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 the memory project secret from the gateway worker path.") print(" rotate-secret Rotate the memory project secret from the gateway worker path.")
print(" Chat secrets use: nap chat secret setup|status|rotate") print(" Chat secrets use: nap chat secret setup|status|rotate; setup uses the same vault/content passphrase.")
return 0 return 0
if subcommand == "process": if subcommand == "process":
return run_wrapper("gateway", ["vault", "process", *rest]) return run_wrapper("gateway", ["vault", "process", *rest])
@@ -687,7 +687,7 @@ def main(argv):
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 the local gateway: nap gateway vault process") print(" Encrypted project setup is completed by the local gateway: nap gateway vault process")
print(" chat Chat secret commands: secret status, secret setup, secret rotate.") print(" chat Chat secret commands: secret status, secret setup, secret rotate.")
print(" Examples: nap chat secret setup, nap chat secret status") print(" Examples: nap chat secret setup --vault-passphrase TEXT, nap chat secret status")
print(" plan Plan commands: list, to-kanban.") print(" plan Plan commands: list, to-kanban.")
print(" Example: nap plan to-kanban /plans/example --column todo") print(" Example: nap plan to-kanban /plans/example --column todo")
print(" lineage Generic source-record -> plan -> kanban lineage checks.") print(" lineage Generic source-record -> plan -> kanban lineage checks.")
+3 -1
View File
@@ -40,7 +40,8 @@ def run():
return {"items": []} return {"items": []}
mod.request_json = request_json mod.request_json = request_json
setup = mod.chat_secret_setup({"project_id": PROJECT_ID}) setup = mod.chat_secret_setup({"project_id": PROJECT_ID, "vault_passphrase": "vault-content"})
assert mod.PROJECT_VAULT_PASSPHRASE == "vault-content"
assert processed and processed[-1]["complete_all"] is True assert processed and processed[-1]["complete_all"] is True
assert setup["chat_secret_configured"] is False assert setup["chat_secret_configured"] is False
@@ -56,6 +57,7 @@ def run():
}] }]
} }
status = mod.chat_secret_status({"project_id": PROJECT_ID}) status = mod.chat_secret_status({"project_id": PROJECT_ID})
assert status["passphrase_authority"] == "vault"
assert status["chat_secret_configured"] is True assert status["chat_secret_configured"] is True
assert status["active_chat_secret"]["version"] == 3 assert status["active_chat_secret"]["version"] == 3
+1 -1
View File
@@ -49,7 +49,7 @@ def run():
writes = [] writes = []
mod.AUTH = {"agent_id": "gateway-agent-1", "account_id": ACCOUNT_ID} mod.AUTH = {"agent_id": "gateway-agent-1", "account_id": ACCOUNT_ID}
mod.GATEWAY_PROJECT_BUNDLE_PASSPHRASE = PASSPHRASE mod.PROJECT_VAULT_PASSPHRASE = PASSPHRASE
mod.VAULT_KEY = bytes(range(32)) mod.VAULT_KEY = bytes(range(32))
mod.VAULT_SECRETS = {} mod.VAULT_SECRETS = {}
mod.persist_vault_secrets = lambda: writes.append(dict(mod.VAULT_SECRETS)) mod.persist_vault_secrets = lambda: writes.append(dict(mod.VAULT_SECRETS))
+1 -1
View File
@@ -115,7 +115,7 @@ def encrypted_node(mod, full_path="/notes/alpha", content="secret needle", metad
def run(): def run():
mod = load_module() mod = load_module()
mod.AUTH = {"account_id": ACCOUNT_ID, "token": "token", "token_expires_at": "later"} mod.AUTH = {"account_id": ACCOUNT_ID, "token": "token", "token_expires_at": "later"}
mod.GATEWAY_PROJECT_BUNDLE_PASSPHRASE = PASSPHRASE mod.PROJECT_VAULT_PASSPHRASE = PASSPHRASE
mod.DEFAULT_PROJECT_ID = "project-1" mod.DEFAULT_PROJECT_ID = "project-1"
mod.MEMORY_SECRET_CACHE.clear() mod.MEMORY_SECRET_CACHE.clear()
mod.ACTIVE_MEMORY_SECRET_VERSIONS.clear() mod.ACTIVE_MEMORY_SECRET_VERSIONS.clear()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Smoke-test vault/content and gateway relay passphrases stay separate."""
import importlib.util
import os
import pathlib
import sys
PROJECT_ID = "11111111-1111-1111-1111-111111111111"
ACCOUNT_ID = "22222222-2222-2222-2222-222222222222"
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_two_passphrase_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()
original_env = dict(os.environ)
try:
os.environ.pop("NAPSEER_VAULT_PASSPHRASE", None)
os.environ.pop("NAPSEER_MASTER_PASSPHRASE", None)
os.environ["NAPSEER_GATEWAY_PASSPHRASE"] = "gateway-only"
mod.PROJECT_VAULT_PASSPHRASE = None
try:
mod.gateway_generate_project_vault_setup_payload(PROJECT_ID, "setup-1")
except RuntimeError as exc:
assert "NAPSEER_VAULT_PASSPHRASE" in str(exc)
else:
raise AssertionError("gateway relay passphrase must not wrap project bundles")
os.environ["NAPSEER_VAULT_PASSPHRASE"] = "vault-content"
mod.AUTH = {"agent_id": "gateway-agent-1", "account_id": ACCOUNT_ID}
payload = mod.gateway_generate_project_vault_setup_payload(PROJECT_ID, "setup-2")
assert {item["secret_kind"] for item in payload["project_secrets"]} == {"chat", "tabs", "gateway", "memory"}
first = payload["project_secrets"][0]["wrapped_key_bundle"]
assert mod.unwrap_project_key_bundle(first)["project_id"] == PROJECT_ID
os.environ["NAPSEER_VAULT_PASSPHRASE"] = "wrong-vault-content"
mod.PROJECT_VAULT_PASSPHRASE = None
try:
mod.unwrap_project_key_bundle(first)
except RuntimeError:
pass
else:
raise AssertionError("project bundle unwrap must require the vault/content passphrase")
mod.PROJECT_VAULT_PASSPHRASE = "manual-vault"
assert mod.project_bundle_passphrase() == "manual-vault"
finally:
os.environ.clear()
os.environ.update(original_env)
print("ok: two passphrase boundary smoke passed")
if __name__ == "__main__":
run()