diff --git a/scripts/agent_token.py b/scripts/agent_token.py new file mode 100644 index 0000000..bb53a5f --- /dev/null +++ b/scripts/agent_token.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +"""Mint (or re-mint) the shared agent credential, non-interactively. + +Creates the `agents` guest user if missing (random throwaway password — agents +authenticate by token, not password), mints a fresh API token for it, and writes +a ready-to-source env file to data/agent.env (chmod 600, gitignored). The token +is never printed to stdout. + +Run from the repo root: + uv run python scripts/agent_token.py # mint, write data/agent.env + uv run python scripts/agent_token.py --revoke # delete ALL of agents' old tokens first + +Agents/macs then use: + source ~/Documents/MODELBEAST/data/agent.env # sets MB_HOST + MB_TOKEN + ./mb ops +(from another tailnet mac: scp m3ultra:~/Documents/MODELBEAST/data/agent.env .) + +The user is a GUEST on purpose: local operators only (flux_local, bg_remove_local, +trellis_mac, sf3d, ...) — all free; the owner's paid fal/OpenRouter keys are +blocked server-side. Mint an owner token from the UI if an agent ever truly needs +a cloud operator. +""" +import argparse +import secrets +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) +from server import auth, db # noqa: E402 + +USERNAME = "agents" +MAX_JOBS = 8 # shared across every agent/mac using this credential +ENV_PATH = ROOT / "data" / "agent.env" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--revoke", action="store_true", + help="revoke all existing tokens for the agents user first") + args = ap.parse_args() + + con = db.connect() + user = auth.get_user_by_name(con, USERNAME) + if user is None: + user = auth.create_user(con, USERNAME, secrets.token_hex(16), + role="guest", max_active_jobs=MAX_JOBS) + print(f"created user '{USERNAME}' (guest, max_jobs={MAX_JOBS})") + else: + print(f"user '{USERNAME}' exists ({user['role']})") + + if args.revoke: + n = con.execute("DELETE FROM api_tokens WHERE user_id = ?", (user["id"],)).rowcount + con.commit() + print(f"revoked {n} old token(s)") + + raw = auth.create_token(con, user["id"], name="shared-agent-cred") + ENV_PATH.parent.mkdir(parents=True, exist_ok=True) + ENV_PATH.write_text( + "# MODELBEAST shared agent credential — source this file, never commit/paste it\n" + "export MB_HOST=http://100.89.131.57:8777\n" + f"export MB_TOKEN={raw}\n") + ENV_PATH.chmod(0o600) + print(f"token written to {ENV_PATH} (not shown; source it to use)") + + +if __name__ == "__main__": + main() diff --git a/server/runner.py b/server/runner.py index 43567bb..63f4c41 100644 --- a/server/runner.py +++ b/server/runner.py @@ -305,6 +305,10 @@ class Runner: def _register_outputs(self, con, outdir: Path, job_id: str) -> list[str]: result_path = outdir / "result.json" registered = [] + # outputs belong to whoever ran the job — guests see only their own + # assets, so registering without user_id hides a guest's results from them + row = con.execute("SELECT user_id FROM jobs WHERE id = ?", (job_id,)).fetchone() + owner_id = row["user_id"] if row else None if result_path.exists(): result = json.loads(result_path.read_text()) for out in result.get("outputs", []): @@ -313,13 +317,14 @@ class Runner: p = outdir / p if p.exists(): a = store.register_file(con, p, name=out.get("name"), - parent_job=job_id, move=True, meta=out.get("meta")) + parent_job=job_id, move=True, meta=out.get("meta"), + user_id=owner_id) registered.append(a["id"]) else: for p in sorted(outdir.iterdir()): if p.name == "result.json" or p.name.startswith("."): continue - a = store.register_file(con, p, parent_job=job_id, move=True) + a = store.register_file(con, p, parent_job=job_id, move=True, user_id=owner_id) registered.append(a["id"]) return registered