Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ hotdata databases list [-w <id>] [-o table|json|yaml]
hotdata databases create --name <name> [--table <table> ...] [--schema public] [-o table|json|yaml]
hotdata databases <name_or_id> [-o table|json|yaml]
hotdata databases delete <name_or_id>
hotdata databases run [--database <id>] [--description <label>] [--schema public] [--table <table> ...] [--expires-at <duration|timestamp>] <cmd> [args...]
hotdata databases <id> run <cmd> [args...]

hotdata databases tables list <database> [--schema <name>] [-o table|json|yaml]
hotdata databases tables load <database> <table> --file ./data.parquet [--schema public]
Expand All @@ -146,6 +148,7 @@ hotdata databases tables delete <database> <table> [--schema public]

- `create` registers a managed connection (`source_type: managed`) with no external credentials. Use `--table` to declare tables up front (required before `tables load` on the current API).
- `tables load` uploads a **parquet** file (or uses a staged `upload_id` from `POST /v1/files`) and publishes it as the table generation (`replace` mode).
- `run` mints a database-scoped JWT and execs `<cmd>` with `HOTDATA_DATABASE_TOKEN`, `HOTDATA_DATABASE_REFRESH_TOKEN`, `HOTDATA_DATABASE`, `HOTDATA_WORKSPACE`, and `HOTDATA_API_URL` injected into its environment. Pass a database id (group-positional `<id>` like `sandbox run`, or `--database <id>`) to scope an existing database; omit both to auto-create a scratch one using `--description` / `--schema` / `--table` / `--expires-at`. Useful for launching an agent or child process whose API access is restricted to a single database.
- For CSV/JSON uploads without a managed database, use `hotdata datasets create` instead (`datasets.main.*`).

Example:
Expand Down
3 changes: 3 additions & 0 deletions skills/hotdata/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ hotdata databases create [--description <label>] [--table <table> ...] [--schema
hotdata databases set <id_or_description>
hotdata databases <id_or_description> [--workspace-id <workspace_id>] [--output table|json|yaml]
hotdata databases delete <id_or_description> [--workspace-id <workspace_id>]
hotdata databases run [--database <id>] [--description <label>] [--schema public] [--table <table> ...] [--expires-at <duration|timestamp>] [--workspace-id <workspace_id>] <cmd> [args...]
hotdata databases <id> run <cmd> [args...]

# Dot-notation shorthand for load: database.table or database.schema.table
hotdata databases load <database.table> [--file ./data.parquet] [--url <url>] [--upload-id <id>] [--workspace-id <workspace_id>]
Expand All @@ -209,6 +211,7 @@ hotdata databases tables delete <table> [--database <id_or_desc>] [--schema publ
- `tables list` — lists tables with `TABLE` (`<database_id>.<schema>.<table>`), `SYNCED`, `LAST_SYNC`. Uses active database when `--database` is omitted.
- `tables load` — uploads a local parquet file (`--file`), a remote parquet URL (`--url`), or a pre-staged upload (`--upload-id`) and publishes with **replace** mode.
- `tables delete` — drops a table from the managed database.
- `run` — mints a database-scoped JWT (via `POST /v1/auth/database`) and execs `<cmd>` with `HOTDATA_DATABASE_TOKEN`, `HOTDATA_DATABASE_REFRESH_TOKEN`, `HOTDATA_DATABASE`, `HOTDATA_WORKSPACE`, and `HOTDATA_API_URL` injected. Pass a database id as a group positional (`hotdata databases <id> run ...`, sandbox-style) or via `--database <id>`; omit both to auto-create a scratch database using `--description` / `--schema` / `--table` / `--expires-at`. Use this to launch an agent or child process whose API access is scoped to a single database. The minted JWT carries `database`, `workspaces`, `permissions:["read","write"]`, `source:"database_token"`. The session is persisted at `~/.hotdata/database_session.json` (mode `0600`); the child's exit code is propagated.

Example:

Expand Down
25 changes: 20 additions & 5 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,33 @@ impl ApiClient {

// Auth source precedence:
//
// 1. `HOTDATA_SANDBOX_TOKEN` env var — a `sandbox run` child
// 1. `HOTDATA_DATABASE_TOKEN` env var — a `databases run` child
// is executing with the parent's credentials scrubbed and a
// database-scoped JWT injected. Refresh in-memory via
// `HOTDATA_DATABASE_REFRESH_TOKEN` near expiry; never write
// to disk (the child's FS may not be writable).
// 2. `HOTDATA_SANDBOX_TOKEN` env var — a `sandbox run` child
// is executing with the parent's credentials scrubbed.
// Refresh in-memory via `HOTDATA_SANDBOX_REFRESH_TOKEN` if
// the JWT is close to expiry; never write to disk (the
// child's FS may not be writable).
// 2. `~/.hotdata/sandbox_session.json` — the user ran
// 3. `~/.hotdata/sandbox_session.json` — the user ran
// `hotdata sandbox set <id>` (or `sandbox new` / `sandbox
// run` in the parent shell). The sandbox JWT is the active
// bearer for *every* command until `sandbox set` (with no
// id) clears the file.
// 3. `~/.hotdata/session.json` + optional api_key fallback —
// 4. `~/.hotdata/session.json` + optional api_key fallback —
// normal user-scoped CLI session.
let api_url = profile_config.api_url.to_string();
let access_token = if std::env::var("HOTDATA_SANDBOX_TOKEN").is_ok() {
let access_token = if std::env::var("HOTDATA_DATABASE_TOKEN").is_ok() {
match crate::database_session::refresh_from_env(&api_url) {
Some(t) => t,
None => {
eprintln!("{}", "error: HOTDATA_DATABASE_TOKEN is empty".red());
std::process::exit(1);
}
}
} else if std::env::var("HOTDATA_SANDBOX_TOKEN").is_ok() {
match crate::sandbox_session::refresh_from_env(&api_url) {
Some(t) => t,
None => {
Expand Down Expand Up @@ -118,7 +131,9 @@ impl ApiClient {
}
profile_config.sandbox
}),
database_id: workspace_id.and_then(|ws| crate::config::load_current_database("default", ws)),
database_id: std::env::var("HOTDATA_DATABASE").ok().or_else(|| {
workspace_id.and_then(|ws| crate::config::load_current_database("default", ws))
}),
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,34 @@ pub enum DatabasesCommands {
#[command(subcommand)]
command: Option<DatabaseTablesCommands>,
},

/// Run a command with a database-scoped token. Creates a new database unless --database is given.
Run {
/// Existing database id to scope the token to (omit to auto-create a database)
#[arg(long)]
database: Option<String>,

/// Description for the auto-created database (only used when --database is omitted)
#[arg(long)]
Comment on lines +642 to +643
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: this user-facing flag is --description but databases create (line 571) uses --name for the same underlying JSON field — both end up serialized as "name" by create_database_request. Two different flag names for the same field on sibling subcommands will trip people up. Picking one (and aligning README/SKILL.md, which currently disagree about which is canonical) would be worth a follow-up. (not blocking)

description: Option<String>,

/// Schema for tables declared in the auto-created database (default: public)
#[arg(long, default_value = "public")]
schema: String,

/// Table to declare in the auto-created database (repeatable)
#[arg(long = "table")]
tables: Vec<String>,

/// When the auto-created database expires. Accepts a relative duration
/// (e.g. 24h, 7d, 90m) or an RFC 3339 timestamp. Defaults to 24h when omitted.
#[arg(long)]
expires_at: Option<String>,

/// Command to execute (everything after `--`)
#[arg(trailing_var_arg = true, required = true)]
cmd: Vec<String>,
},
}

#[derive(Subcommand)]
Expand Down
Loading
Loading