Secret Service¶
Secrets are the API keys, tokens and options a household sets for a package from the mobile app. A package reads and writes them through the SDK's JarvisStorage; on the Python node they live in the node's encrypted database.
Declaring secrets vs. reading secrets
This page covers the runtime API for reading and writing secrets. For how to declare what secrets your command needs, see Secrets Deep Dive.
Reading and writing secrets¶
from jarvis_command_sdk import JarvisStorage
storage = JarvisStorage("get_weather") # your command's name
api_key = storage.get_secret("OPENWEATHER_API_KEY") # integration scope (the default)
email = storage.get_secret("GMAIL_ADDRESS", scope="user") # the current user's own value
storage.set_secret("LAST_CITY", "Denver") # stored as a string
storage.set_secret("RETRIES", "3", value_type="int")
storage.delete_secret("LAST_CITY")
get_secret returns the value as a string, or None when it isn't set. Values are always strings: convert an int or bool yourself. With no storage backend registered (tests, Pantry validation) get_secret returns None and the writes do nothing.
To make "user" the default scope for an instance, pass it once: JarvisStorage("gmail", secret_scope="user").
Scopes¶
A secret has one of two scopes. Any other value raises ValueError when the JarvisSecret is declared.
| Scope | One value per | Use for |
|---|---|---|
"integration" |
Household | API keys, service URLs, shared preferences such as units |
"user" |
Household member | Personal accounts and credentials, such as one person's email |
For a "user" secret, JarvisStorage looks up the user from the request that is running, so each member gets their own value. Outside a request (an agent's background run, for example) there is no current user: store a household member's id in a value_type="user" secret and act for that member.
Value types¶
| Type | Stored as | Mobile app field |
|---|---|---|
"string" |
Text | Text field (a picker with enum_values) |
"int" |
Text, convert with int() |
Number |
"bool" |
"true" / "false" |
Toggle |
"user" |
A user id as text | Household-member picker |
The type is metadata for the settings screen; every value is stored as text.
Typical usage in a command¶
from typing import List
from jarvis_command_sdk import (
CommandExample,
CommandResponse,
IJarvisCommand,
JarvisParameter,
JarvisSecret,
JarvisStorage,
RequestInformation,
)
class GetStockPrice(IJarvisCommand):
@property
def command_name(self) -> str:
return "get_stock_price"
@property
def description(self) -> str:
return "Get the current price of a stock"
@property
def keywords(self) -> List[str]:
return ["stock", "price", "shares"]
@property
def parameters(self) -> List[JarvisParameter]:
return [JarvisParameter("symbol", "string", required=True, description="Ticker symbol")]
@property
def required_secrets(self) -> List[JarvisSecret]:
return [
JarvisSecret("FINANCE_API_KEY", "Financial data API key",
"integration", "string", friendly_name="API Key"),
JarvisSecret("DEFAULT_CURRENCY", "Default display currency",
"integration", "string",
required=False, is_sensitive=False, friendly_name="Currency"),
]
def generate_prompt_examples(self) -> List[CommandExample]:
return [CommandExample("What's Apple trading at?", {"symbol": "AAPL"}, is_primary=True)]
def generate_adapter_examples(self) -> List[CommandExample]:
return self.generate_prompt_examples()
def run(self, request_info: RequestInformation, **kwargs) -> CommandResponse:
storage = JarvisStorage(self.command_name)
api_key = storage.get_secret("FINANCE_API_KEY")
if not api_key:
return CommandResponse.error_response(error_details="Set the API key in the mobile app first.")
currency = storage.get_secret("DEFAULT_CURRENCY") or "USD" # optional
symbol = kwargs["symbol"]
price = 123.45 # call your price API with api_key here
return CommandResponse.success_response(
context_data={"message": f"{symbol} is trading at {price} {currency}"}
)
The command's name is the command_name property. A host may also refuse to run a command whose required secrets are missing (the SDK's execute() raises MissingSecretsError when it is given the secrets), but check for None anyway: the household may not have set a value yet.
On the Python node¶
JarvisStorage on a Pi is backed by the node's own services/secret_service.py (in jarvis-node-setup). The node's built-in commands call it directly; packages should not, since a computer doesn't have it.
| Function | Does |
|---|---|
get_secret_value(key, scope, user_id=None) |
The value, or None |
get_secret_value_int(key, scope, user_id=None) |
The value as an int, or None |
set_secret(key, value, scope, value_type="string", user_id=None) |
Insert or update |
delete_secret(key, scope, user_id=None) |
Delete |
get_all_secrets(scope, user_id=None) |
Every secret in a scope |
ensure_secret_exists(key, scope, value_type) |
Create an empty row if missing |
seed_command_secrets(secrets) |
Empty rows for a command's declared secrets (skips "user" ones); existing values are kept |
Each call opens and closes its own database session, so it is safe from any thread. Rows hold key, value, scope, value_type and, for user-scoped secrets, user_id.
On the node, python scripts/install_command.py --all seeds empty rows for every command's secrets (it is safe to re-run), and --list lists them.
The database is SQLCipher (sqlcipher3). Its key is ~/.jarvis/db.key, created the first time the database is opened, and the file is ./jarvis_node.db in the node's working directory (or $JARVIS_NODE_DB). Without the key the database is unreadable.