🔑 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]:
|
def _register_outputs(self, con, outdir: Path, job_id: str) -> list[str]:
|
||||||
result_path = outdir / "result.json"
|
result_path = outdir / "result.json"
|
||||||
registered = []
|
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():
|
if result_path.exists():
|
||||||
result = json.loads(result_path.read_text())
|
result = json.loads(result_path.read_text())
|
||||||
for out in result.get("outputs", []):
|
for out in result.get("outputs", []):
|
||||||
@ -313,13 +317,14 @@ class Runner:
|
|||||||
p = outdir / p
|
p = outdir / p
|
||||||
if p.exists():
|
if p.exists():
|
||||||
a = store.register_file(con, p, name=out.get("name"),
|
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"])
|
registered.append(a["id"])
|
||||||
else:
|
else:
|
||||||
for p in sorted(outdir.iterdir()):
|
for p in sorted(outdir.iterdir()):
|
||||||
if p.name == "result.json" or p.name.startswith("."):
|
if p.name == "result.json" or p.name.startswith("."):
|
||||||
continue
|
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"])
|
registered.append(a["id"])
|
||||||
return registered
|
return registered
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user