Separate vault and gateway passphrases
This commit is contained in:
@@ -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.")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user