🔑 agents get keys: shared agent token minting + guest-visible job outputs
scripts/agent_token.py mints the 'agents' guest user + token non-interactively, writes data/agent.env (0600, gitignored, never printed). Fixes _register_outputs dropping user_id — guest job outputs were registered ownerless, so guests could never see or download their own results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b8c333a93e
commit
19cf9ed21e
68
scripts/agent_token.py
Normal file
68
scripts/agent_token.py
Normal file
@ -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()
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user