Simplify chat secret CLI flow

This commit is contained in:
HumbleTurtle
2026-05-19 17:42:44 -03:00
parent 4948646b9a
commit e24daf54c1
6 changed files with 297 additions and 24 deletions
+13 -5
View File
@@ -598,14 +598,18 @@ def handle_gateway(args):
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 process Complete pending setup requests.")
print(" nap gateway vault rotate-secret --kind chat|tabs|gateway|memory")
print(" nap gateway vault rotate-secret --kind memory")
print(" nap chat secret setup Configure chat secret state through the gateway.")
return 0
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 memory] [--all] [--project-id ID]")
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(" rotate-secret Rotate one encrypted project secret.")
print(" rotate-secret Rotate the memory project secret from the gateway worker path.")
print(" Chat secrets use: nap chat secret setup|status|rotate")
return 0
if subcommand == "process":
return run_wrapper("gateway", ["vault", "process", *rest])
if subcommand == "start":
return print_json(start_service("gateway", ["--no-browser", *rest])) or 0
if subcommand == "stop":
@@ -638,6 +642,8 @@ def main(argv):
return 0
if command == "gateway":
return handle_gateway(args)
if command == "chat":
return run_wrapper("chat", args or ["secret", "status"])
if command == "status":
return run_wrapper("configure", args)
if command == "project":
@@ -674,19 +680,21 @@ def main(argv):
)
return 0
if command in {"help", "-h", "--help"}:
print("Usage: nap [auth|project|plan|lineage|gateway|agent|feedback|status|update|where|install]")
print("Usage: nap [auth|project|chat|plan|lineage|gateway|agent|feedback|status|update|where|install]")
print(" auth Auth commands: login-local, status.")
print(" Example: nap auth login-local")
print(" project Project commands: create, status, encryption.")
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(" chat Chat secret commands: secret status, secret setup, secret rotate.")
print(" Examples: nap chat secret setup, nap chat secret status")
print(" plan Plan commands: list, to-kanban.")
print(" Example: nap plan to-kanban /plans/example --column todo")
print(" lineage Generic source-record -> plan -> kanban lineage checks.")
print(" Example: nap lineage status")
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(" 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 process --all")
print(" agent Agent commands: list, workspaces, create, show, edit.")
print(" list shows registered project agents; workspaces shows /agents folders.")
print(" feedback Feedback commands: list, global, status, resolve.")
+179 -5
View File
@@ -3874,6 +3874,12 @@ def gateway_rotate_project_vault_secret(args=None):
secret_kind = str(args.get("secret_kind") or args.get("kind") or "chat").strip().lower()
if secret_kind not in GATEWAY_PROJECT_SECRET_KINDS:
raise RuntimeError("secret_kind must be chat, tabs, gateway, or memory")
if secret_kind != MEMORY_SECRET_KIND:
raise RuntimeError(
f"{secret_kind} secret rotation requires operator authorization and is not available "
"through the gateway worker token. Use `nap chat secret setup` to initialize chat "
"secret state, and rotate chat secrets from an operator-authorized session."
)
current = request_json("GET", f"/v1/projects/{project_id}/vault/secrets/{urllib.parse.quote(secret_kind, safe='')}/active")
if current.get("secret_kind") != secret_kind:
raise RuntimeError("active project secret is unavailable")
@@ -3931,6 +3937,110 @@ def gateway_rotate_project_vault_secret(args=None):
return result
def project_secret_versions(project_id, secret_kind=None):
path = f"/v1/projects/{project_id}/vault/secrets"
if secret_kind:
path = f"{path}?secret_kind={urllib.parse.quote(str(secret_kind), safe='')}"
return request_json("GET", path)
def chat_secret_status(args=None):
args = args or {}
project_id = str(args.get("project_id") or current_project_id()).strip()
vault = gateway_vault_status({"project_id": project_id})
response = {
"status": "ok",
"project_id": project_id,
"locked": vault.get("locked"),
"vault_configured": vault.get("vault_configured"),
"pending_setup_requests": vault.get("pending_setup_requests", 0),
"chat_secret_configured": None,
"active_chat_secret": None,
}
try:
versions = project_secret_versions(project_id, "chat").get("items", [])
except Exception as exc:
return {
**response,
"status": "operator_auth_required",
"message": (
"Could not read chat secret status with the current token. "
"Chat/tabs/gateway secret status and rotation require operator authorization. "
"Run `nap auth login-local`, then retry `nap chat secret status`."
),
"error": str(exc),
}
active = next(
(
item for item in versions
if item.get("secret_kind") == "chat"
and item.get("status") == "active"
and item.get("activation_status") == "active"
),
None,
)
if active:
return {
**response,
"chat_secret_configured": True,
"active_chat_secret": {
"version": active.get("version"),
"last_rotated_at": active.get("last_rotated_at"),
"storage_provider": active.get("storage_provider"),
},
"message": "Chat secret is configured.",
}
if response["pending_setup_requests"]:
return {
**response,
"chat_secret_configured": False,
"message": "Chat secret setup is pending. Run `nap chat secret setup`.",
}
return {
**response,
"chat_secret_configured": False,
"message": (
"No active chat secret and no pending gateway vault setup request. "
"Enable encrypted project setup from the cloud UI, then run `nap chat secret setup`."
),
}
def chat_secret_setup(args=None):
args = args or {}
project_id = str(args.get("project_id") or current_project_id()).strip()
status = chat_secret_status({"project_id": project_id})
if status.get("chat_secret_configured") is True:
return status
if status.get("locked"):
return {
**status,
"status": "locked",
"message": "Gateway vault is locked. Run `nap gateway setup` or unlock the gateway, then retry `nap chat secret setup`.",
}
if status.get("pending_setup_requests", 0) > 0:
processed = gateway_process_vault_setup_requests({"project_id": project_id, "complete_all": True})
return {
**chat_secret_status({"project_id": project_id}),
"processed_setup": processed,
}
return status
def chat_secret_rotate(args=None):
args = args or {}
project_id = str(args.get("project_id") or current_project_id()).strip()
return {
"status": "operator_auth_required",
"project_id": project_id,
"message": (
"Chat secret rotation is not available through the gateway worker token. "
"This preserves the boundary that worker tokens cannot read active chat secrets. "
"Use an operator-authorized rotation flow."
),
}
def gateway_vault_setup_loop():
interval = max(10, min(int(os.environ.get("NAPSEER_GATEWAY_VAULT_SETUP_POLL_SECONDS", "30")), 300))
gateway_log("gateway_vault_setup_thread_started", project_id=current_project_id())
@@ -10273,17 +10383,44 @@ def raw_tools():
},
{
"name": "nap_gateway_vault_secret_rotate",
"description": "Rotate one project secret version from this local gateway by uploading an opaque client-wrapped key bundle record for backend-owned HashiCorp storage.",
"description": "Rotate the memory project secret version from this local gateway by uploading an opaque client-wrapped key bundle record for backend-owned HashiCorp storage. Chat, tabs, and gateway secrets require operator authorization.",
"inputSchema": {
"type": "object",
"required": ["secret_kind"],
"properties": {
"project_id": {"type": "string"},
"secret_kind": {"type": "string", "enum": ["chat", "tabs", "gateway", "memory"]},
"secret_kind": {"type": "string", "enum": ["memory"]},
},
"additionalProperties": False,
},
},
{
"name": "nap_chat_secret_status",
"description": "Show chat secret setup status without exposing secret material.",
"inputSchema": {
"type": "object",
"properties": {"project_id": {"type": "string"}},
"additionalProperties": False,
},
},
{
"name": "nap_chat_secret_setup",
"description": "Complete pending gateway vault setup needed for chat secrets, or report the next required action.",
"inputSchema": {
"type": "object",
"properties": {"project_id": {"type": "string"}},
"additionalProperties": False,
},
},
{
"name": "nap_chat_secret_rotate",
"description": "Explain the operator authorization required to rotate chat secrets.",
"inputSchema": {
"type": "object",
"properties": {"project_id": {"type": "string"}},
"additionalProperties": False,
},
},
{
"name": "nap_gateway_restart",
"description": "Kill and relaunch the managed gateway tmux window using the configured default command.",
@@ -11238,6 +11375,9 @@ TOOL_CATEGORIES = {
"nap_gateway_vault_ls",
"nap_gateway_vault_process",
"nap_gateway_vault_secret_rotate",
"nap_chat_secret_status",
"nap_chat_secret_setup",
"nap_chat_secret_rotate",
"nap_gateway_restart",
"nap_gateway_kill",
"nap_ui_open",
@@ -11266,6 +11406,7 @@ def tool_category(name):
READ_ONLY_TOOLS = {
"nap_whoami",
"nap_gateway_status",
"nap_chat_secret_status",
"nap_update_status",
"nap_apropos",
"nap_man",
@@ -11312,6 +11453,9 @@ LOCAL_FILE_TOOLS = {
"nap_gateway_configure",
"nap_gateway_vault_process",
"nap_gateway_vault_secret_rotate",
"nap_chat_secret_status",
"nap_chat_secret_setup",
"nap_chat_secret_rotate",
}
PROCESS_CONTROL_TOOLS = {"nap_ui_open"}
@@ -11587,6 +11731,12 @@ def call_tool_impl(name, args):
return gateway_process_vault_setup_requests(args)
if name == "nap_gateway_vault_secret_rotate":
return gateway_rotate_project_vault_secret(args)
if name == "nap_chat_secret_status":
return chat_secret_status(args)
if name == "nap_chat_secret_setup":
return chat_secret_setup(args)
if name == "nap_chat_secret_rotate":
return chat_secret_rotate(args)
if name == "nap_gateway_restart":
return gateway_terminal_restart(args)
if name == "nap_gateway_kill":
@@ -12092,7 +12242,8 @@ def cli_main(argv):
action = args[0] if args else "status"
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|memory] [--all] [--project-id ID]")
print("Usage: napseer_mcp_server.py gateway vault [status|list|process|rotate-secret] [--kind memory] [--all] [--project-id ID]")
print("Chat secrets use: nap chat secret setup|status|rotate")
return
payload = {
"project_id": cli_option(vault_args, "--project-id", default=None),
@@ -12107,7 +12258,7 @@ def cli_main(argv):
return
if action == "rotate-secret":
if not payload.get("secret_kind"):
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 "memory"
print(json.dumps(gateway_rotate_project_vault_secret(payload), indent=2))
return
raise RuntimeError("unknown gateway vault action; use status|list|process|rotate-secret")
@@ -12122,7 +12273,30 @@ def cli_main(argv):
"new_passphrase": cli_option(args, "--new-passphrase", "--passphrase", default=None),
}), 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|kill|setup|rotate-passphrase --new-passphrase TEXT]")
print("Usage: napseer_mcp_server.py gateway [status|configure [--command CMD] [--target session:window.pane]|vault [status|list|process|rotate-secret] [--kind memory] [--all]|terminal [list|create|close|capture|input]|schedule [list|create|update|delete|run]|restart|kill|setup|rotate-passphrase --new-passphrase TEXT]")
return
if command == "chat":
subcommand = argv[2] if len(argv) > 2 else "secret"
args = argv[3:]
if subcommand == "secret":
action = args[0] if args else "status"
secret_args = args[1:] if args else []
else:
action = subcommand
secret_args = args
payload = {
"project_id": cli_option(secret_args, "--project-id", default=None),
}
if action in {"status", "show", "ls", "list"}:
print(json.dumps(chat_secret_status(payload), indent=2))
return
if action in {"setup", "configure", "init"}:
print(json.dumps(chat_secret_setup(payload), indent=2))
return
if action in {"rotate", "rotate-secret"}:
print(json.dumps(chat_secret_rotate(payload), indent=2))
return
print("Usage: napseer_mcp_server.py chat secret [status|setup|rotate] [--project-id ID]")
return
if command == "feedback":
subcommand = argv[2] if len(argv) > 2 else "list"