InnerI commited on
Commit
7d40e0c
·
verified ·
1 Parent(s): 0906221

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -188
app.py CHANGED
@@ -1,10 +1,8 @@
1
-
2
  import os, json, time, hmac, hashlib, sqlite3, base64, secrets
3
  from typing import Any, Dict, List, Optional, Tuple
4
 
5
  import gradio as gr
6
  from jsonschema import validate, ValidationError
7
- import jwt
8
  import stripe
9
  from fastapi import FastAPI, Request, HTTPException
10
  from fastapi.responses import JSONResponse
@@ -18,7 +16,7 @@ import ast, operator as op, datetime as dt
18
  APP_TITLE = "Inner I Studio — Secure Agent Hub"
19
  DB_PATH = os.getenv("INNERI_DB_PATH", "inneri_studio.db")
20
  RECEIPT_SIGNING_KEY = os.getenv("INNERI_RECEIPT_SIGNING_KEY", "dev_only_change_me")
21
- JWT_SIGNING_KEY = os.getenv("INNERI_JWT_SIGNING_KEY", "dev_jwt_change_me")
22
  ADMIN_PASSWORD = os.getenv("INNERI_ADMIN_PASSWORD", "admin")
23
 
24
  # Stripe (optional)
@@ -96,22 +94,24 @@ def gen_ed25519_keypair() -> Tuple[str, str]:
96
  ).decode("utf-8")
97
  return priv_pem, pub_pem
98
 
99
- def seed_tools(conn: sqlite3.Connection):
100
  tools = [
101
  ("echo", "Echo", "Returns the provided text.", "low",
102
- {"type":"object","properties":{"text":{"type":"string","maxLength":2000}},"required":["text"],"additionalProperties":False}),
103
  ("time_now", "Time Now", "Returns server time (UTC).", "low",
104
- {"type":"object","properties":{},"additionalProperties":False}),
105
  ("math_eval", "Math Eval", "Evaluates a simple arithmetic expression.", "med",
106
- {"type":"object","properties":{"expression":{"type":"string","maxLength":200}},"required":["expression"],"additionalProperties":False}),
107
  ]
108
  cur = conn.cursor()
109
  for t in tools:
110
- cur.execute("INSERT OR IGNORE INTO tools(tool_id,name,description,risk,json_schema,enabled,version) VALUES(?,?,?,?,?,?,?)",
111
- (t[0], t[1], t[2], t[3], json.dumps(t[4]), 1, 1))
 
 
112
  conn.commit()
113
 
114
- def init_db():
115
  conn = db()
116
  cur = conn.cursor()
117
  cur.executescript("""
@@ -184,8 +184,10 @@ def init_db():
184
  cur.execute("SELECT user_id FROM users WHERE email=?", ("admin@local",))
185
  if not cur.fetchone():
186
  ph = sha256_hex("admin@local:" + ADMIN_PASSWORD)
187
- cur.execute("INSERT INTO users(email,password_hash,role,tier,credits,created_at) VALUES(?,?,?,?,?,?)",
188
- ("admin@local", ph, "admin", "pro", 0, now_unix()))
 
 
189
  conn.commit()
190
 
191
  # seed tools
@@ -197,24 +199,43 @@ def init_db():
197
  cur.execute("SELECT agent_id FROM agents WHERE agent_id='agent_public_01'")
198
  if not cur.fetchone():
199
  _, pub = gen_ed25519_keypair()
200
- cur.execute("INSERT INTO agents(agent_id,display_name,verification_level,risk_tier,public_key_pem,created_at) VALUES(?,?,?,?,?,?)",
201
- ("agent_public_01", "Inner I Public Agent", "none", "low", pub, now_unix()))
202
- cur.execute("INSERT OR IGNORE INTO reputations(agent_id,score,updated_at) VALUES(?,?,?)",
203
- ("agent_public_01", 50, now_unix()))
 
 
 
 
204
  conn.commit()
205
 
206
  conn.close()
207
 
208
- def audit_append(actor_agent_id: Optional[str], actor_user_id: Optional[int], action: str, request_obj: Dict[str, Any], result_obj: Dict[str, Any]) -> Dict[str, Any]:
 
 
 
 
 
 
209
  conn = db()
210
  cur = conn.cursor()
211
  cur.execute("SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1")
212
  row = cur.fetchone()
213
  prev_hash = row["row_hash"] if row else None
214
- row_obj = {"actor_agent_id": actor_agent_id, "actor_user_id": actor_user_id, "action": action, "request": request_obj, "result": result_obj, "prev_hash": prev_hash}
 
 
 
 
 
 
 
215
  row_hash = sha256_hex(canonical_json(row_obj))
216
- cur.execute("INSERT INTO audit_log(ts,actor_agent_id,actor_user_id,action,request_json,result_json,prev_hash,row_hash) VALUES(?,?,?,?,?,?,?,?)",
217
- (now_unix(), actor_agent_id, actor_user_id, action, json.dumps(request_obj), json.dumps(result_obj), prev_hash, row_hash))
 
 
218
  conn.commit()
219
  audit_id = cur.lastrowid
220
  conn.close()
@@ -230,11 +251,14 @@ def signup(email: str, password: str) -> str:
230
  raise gr.Error("Enter a valid email")
231
  if len(password) < 8:
232
  raise gr.Error("Password must be 8+ chars")
 
233
  conn = db()
234
  cur = conn.cursor()
235
  try:
236
- cur.execute("INSERT INTO users(email,password_hash,role,tier,credits,created_at) VALUES(?,?,?,?,?,?)",
237
- (email, hash_pw(email, password), "user", "free", 0, now_unix()))
 
 
238
  conn.commit()
239
  except sqlite3.IntegrityError:
240
  conn.close()
@@ -249,16 +273,22 @@ def login(email: str, password: str) -> Tuple[str, str]:
249
  cur.execute("SELECT user_id,password_hash FROM users WHERE email=?", (email,))
250
  row = cur.fetchone()
251
  conn.close()
 
252
  if not row:
253
  return "", "❌ user_not_found"
254
  if hash_pw(email, password) != row["password_hash"]:
255
  return "", "❌ bad_password"
 
256
  token = b64url(secrets.token_bytes(24))
257
  now = now_unix()
258
  exp = now + 3600 * 8
 
259
  conn = db()
260
  cur = conn.cursor()
261
- cur.execute("INSERT INTO sessions(token,user_id,created_at,expires_at) VALUES(?,?,?,?)", (token, row["user_id"], now, exp))
 
 
 
262
  conn.commit()
263
  conn.close()
264
  return token, "✅ logged_in"
@@ -266,11 +296,14 @@ def login(email: str, password: str) -> Tuple[str, str]:
266
  def get_session_user(session_token: str) -> Dict[str, Any]:
267
  conn = db()
268
  cur = conn.cursor()
269
- cur.execute("""
 
270
  SELECT u.user_id,u.email,u.role,u.tier,u.credits,u.subscription_status,s.expires_at
271
  FROM sessions s JOIN users u ON u.user_id=s.user_id
272
  WHERE s.token=?
273
- """, (session_token,))
 
 
274
  row = cur.fetchone()
275
  conn.close()
276
  if not row:
@@ -295,17 +328,18 @@ def check_and_increment_run(user: Dict[str, Any]) -> None:
295
 
296
  conn = db()
297
  cur = conn.cursor()
 
298
  cur.execute("INSERT OR IGNORE INTO usage_daily(user_id,day,runs) VALUES(?,?,0)", (user["user_id"], day))
299
  cur.execute("SELECT runs FROM usage_daily WHERE user_id=? AND day=?", (user["user_id"], day))
300
  runs = int(cur.fetchone()["runs"])
301
 
302
- # Always increment the run counter once we allow the run.
303
  if runs >= limit and user["role"] != "admin":
304
- # Free tier may continue using credits (optional)
305
  if user["tier"] == "free" and user["credits"] >= RUN_CREDIT_COST:
306
- # consume credits (pay-as-you-go burst)
307
- cur.execute("UPDATE users SET credits=credits-? WHERE user_id=? AND credits>=?",
308
- (RUN_CREDIT_COST, user["user_id"], RUN_CREDIT_COST))
 
309
  if cur.rowcount != 1:
310
  conn.close()
311
  raise gr.Error(f"Daily limit reached ({limit}). Upgrade to {PLANS['pro']['name']} or buy credits.")
@@ -316,30 +350,32 @@ def check_and_increment_run(user: Dict[str, Any]) -> None:
316
  cur.execute("UPDATE usage_daily SET runs=runs+1 WHERE user_id=? AND day=?", (user["user_id"], day))
317
  conn.commit()
318
  conn.close()
319
- raise gr.Error(f"Daily limit reached ({limit}). Upgrade to Pro.")
320
- cur.execute("UPDATE usage_daily SET runs=runs+1 WHERE user_id=? AND day=?", (user["user_id"], day))
321
- conn.commit()
322
- conn.close()
323
-
324
- def maybe_charge_credit(user: Dict[str, Any]) -> None:
325
- # Credits are consumed only when a free user exceeds the daily limit (burst mode).
326
- return
327
- raise gr.Error("No credits left. Buy a credit pack or upgrade to Pro.")
328
- conn.commit()
329
- conn.close()
330
 
331
  # ---------------- Policy + tools ----------------
332
  def policy_decide(agent: Dict[str, Any], request: Dict[str, Any]) -> Dict[str, Any]:
333
  if "secret" in (request.get("data_scopes") or []):
334
  return {"allow": False, "mode": "deny", "reasons": ["secret_scope_blocked"]}
 
335
  tools = request.get("tools") or []
336
  if agent.get("risk_tier") == "high" or any(t.get("risk") == "high" for t in tools):
337
  return {"allow": False, "mode": "deny", "reasons": ["high_risk_blocked"]}
 
338
  if agent.get("verification_level") == "none" or any(t.get("risk") == "med" for t in tools):
339
  return {"allow": True, "mode": "sandbox", "reasons": ["sandbox_unverified_or_med"]}
 
340
  return {"allow": True, "mode": "normal", "reasons": []}
341
 
342
- _OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg, ast.Mod: op.mod, ast.FloorDiv: op.floordiv}
 
 
 
 
 
 
 
 
 
 
343
  def _eval(node):
344
  if isinstance(node, ast.Num):
345
  return node.n
@@ -361,8 +397,10 @@ def run_tool(tool_id: str, args: Dict[str, Any]) -> Dict[str, Any]:
361
 
362
  def secure_call(user: Dict[str, Any], agent_id: str, intent: str, tools: List[Dict[str, Any]]) -> Dict[str, Any]:
363
  check_and_increment_run(user)
 
364
  conn = db()
365
  cur = conn.cursor()
 
366
  cur.execute("SELECT * FROM agents WHERE agent_id=?", (agent_id,))
367
  a = cur.fetchone()
368
  if not a:
@@ -370,7 +408,7 @@ def secure_call(user: Dict[str, Any], agent_id: str, intent: str, tools: List[Di
370
  raise gr.Error("agent_not_found")
371
  agent = dict(a)
372
 
373
- tools_meta = []
374
  for tc in tools:
375
  cur.execute("SELECT * FROM tools WHERE tool_id=?", (tc["tool_id"],))
376
  t = cur.fetchone()
@@ -379,78 +417,60 @@ def secure_call(user: Dict[str, Any], agent_id: str, intent: str, tools: List[Di
379
  raise gr.Error(f"tool_not_found_or_disabled: {tc['tool_id']}")
380
  tools_meta.append({"tool_id": t["tool_id"], "risk": t["risk"], "schema": json.loads(t["json_schema"])})
381
 
382
- decision = policy_decide(agent, {"intent": intent, "tools": [{"tool_id": tm["tool_id"], "risk": tm["risk"]} for tm in tools_meta], "data_scopes": ["public"]})
 
 
 
383
  if not decision["allow"]:
384
  conn.close()
385
  audit_append(agent_id, user["user_id"], "secure_call.deny", {"intent": intent, "tools": tools}, {"decision": decision})
386
  return {"denied": True, "decision": decision}
387
 
388
  mode = decision["mode"]
389
- outputs = []
390
- # Tier-based tool risk gating (monetization + safety)
391
- tier = user["tier"] if user["role"] != "admin" else "enterprise"
392
- if tier not in PLANS:
393
- tier = "free"
394
- tier_max = PLANS[tier]["max_tool_risk"]
395
- risk_rank = {"low": 1, "med": 2, "high": 3}
396
- tier_max_rank = risk_rank[tier_max]
397
- for tc, tm in zip(tools, tools_meta):
 
 
398
  try:
399
  validate(instance=tc.get("args", {}), schema=tm["schema"])
400
  except ValidationError:
401
  conn.close()
402
  raise gr.Error(f"args_schema_invalid: {tm['tool_id']}")
403
 
404
- # Plan gating: block tools above tier max risk
405
- if risk_rank.get(tm["risk"], 3) > tier_max_rank:
406
- outputs.append({
407
- "tool_id": tm["tool_id"],
408
- "blocked": True,
409
- "reason": "plan_blocks_{tm['risk']}_risk"
410
- })
411
- continue
412
-
413
- # Sandbox gating (policy-based)
414
- if mode == "sandbox" and tm["risk"] != "low":
415
- outputs.append({
416
- "tool_id": tm["tool_id"],
417
- "blocked": True,
418
- "reason": "sandbox_mode"
419
- })
420
- continue
421
 
422
- try:
423
- outputs.append({
424
- "tool_id": tm["tool_id"],
425
- "output": run_tool(tm["tool_id"], tc.get("args", {}))
426
- })
427
- except Exception as e:
428
- outputs.append({
429
- "tool_id": tm["tool_id"],
430
- "error": str(e)
431
- })
432
-
433
- # receipts + audit chain (MUST be after the loop)
434
- receipt = {
435
- "ts_unix": now_unix(),
436
- "agent_id": agent_id,
437
- "user_id": user["user_id"],
438
- "intent": intent,
439
- "mode": mode,
440
- "decision": decision,
441
- "outputs_hash": sha256_hex(canonical_json(outputs)),
442
- }
443
- receipt["signature"] = sign_hmac(receipt, RECEIPT_SIGNING_KEY)
444
- audit = audit_append(
445
- agent_id,
446
- user["user_id"],
447
- "secure_call.run",
448
- {"intent": intent, "tools": tools},
449
- {"outputs": outputs, "receipt": receipt},
450
- )
451
-
452
- conn.close()
453
- return {"outputs": outputs, "receipt": receipt, "audit": audit}
454
 
455
  # ---------------- Admin actions ----------------
456
  def create_agent_admin(session_token: str, agent_id: str, display_name: str, risk_tier: str, verification_level: str):
@@ -458,8 +478,10 @@ def create_agent_admin(session_token: str, agent_id: str, display_name: str, ris
458
  priv_pem, pub_pem = gen_ed25519_keypair()
459
  conn = db()
460
  cur = conn.cursor()
461
- cur.execute("INSERT INTO agents(agent_id,display_name,verification_level,risk_tier,public_key_pem,created_at) VALUES(?,?,?,?,?,?)",
462
- (agent_id, display_name, verification_level, risk_tier, pub_pem, now_unix()))
 
 
463
  conn.commit()
464
  conn.close()
465
  return "✅ agent_created (save private key)", priv_pem, pub_pem
@@ -480,7 +502,7 @@ def list_tools_admin(session_token: str):
480
  cur.execute("SELECT tool_id,name,risk,enabled,version,description FROM tools ORDER BY tool_id")
481
  rows = cur.fetchall()
482
  conn.close()
483
- return [[r["tool_id"], r["name"], r["risk"], "yes" if int(r["enabled"])==1 else "no", str(r["version"]), r["description"]] for r in rows]
484
 
485
  def upsert_tool_admin(session_token: str, tool_id: str, name: str, description: str, risk: str, enabled: bool, schema_json: str):
486
  require_admin(session_token)
@@ -490,12 +512,15 @@ def upsert_tool_admin(session_token: str, tool_id: str, name: str, description:
490
  raise ValueError("schema.type must be object")
491
  except Exception as e:
492
  raise gr.Error(f"Invalid schema JSON: {e}")
 
493
  conn = db()
494
  cur = conn.cursor()
495
  cur.execute("SELECT version FROM tools WHERE tool_id=?", (tool_id,))
496
  row = cur.fetchone()
497
  version = (int(row["version"]) + 1) if row else 1
498
- cur.execute("""
 
 
499
  INSERT INTO tools(tool_id,name,description,risk,json_schema,enabled,version)
500
  VALUES(?,?,?,?,?,?,?)
501
  ON CONFLICT(tool_id) DO UPDATE SET
@@ -505,7 +530,9 @@ def upsert_tool_admin(session_token: str, tool_id: str, name: str, description:
505
  json_schema=excluded.json_schema,
506
  enabled=excluded.enabled,
507
  version=?
508
- """, (tool_id, name, description, risk, json.dumps(schema), 1 if enabled else 0, version, version))
 
 
509
  conn.commit()
510
  conn.close()
511
  return f"✅ saved_tool `{tool_id}` v{version}"
@@ -522,8 +549,8 @@ def read_audit_admin(session_token: str, limit: int):
522
  def admin_set_user_tier(session_token: str, email: str, tier: str) -> str:
523
  require_admin(session_token)
524
  email = email.strip().lower()
525
- if tier not in ("free","pro"):
526
- raise gr.Error("tier must be free or pro")
527
  conn = db()
528
  cur = conn.cursor()
529
  cur.execute("UPDATE users SET tier=? WHERE email=?", (tier, email))
@@ -535,23 +562,23 @@ def admin_set_user_tier(session_token: str, email: str, tier: str) -> str:
535
  # ---------------- Billing ----------------
536
  def user_me(session_token: str) -> str:
537
  u = get_session_user(session_token)
538
- safe = {k: u[k] for k in ["user_id","email","role","tier","credits","subscription_status"]}
539
- safe["plan_name"] = PLANS.get(safe["tier"], PLANS["free"])["name"] if safe["role"] != "admin" else PLANS["enterprise"]["name"]
540
- safe["stripe_enabled"] = STRIPE_ENABLED
541
- return json.dumps(safe, indent=2)
542
 
543
  def create_checkout_session(session_token: str, kind: str) -> str:
544
  if not STRIPE_ENABLED:
545
  raise gr.Error("Stripe not configured (set STRIPE_SECRET_KEY + PUBLIC_BASE_URL).")
546
  u = get_session_user(session_token)
547
 
548
- # Subscriptions
549
  if kind == "pro":
550
  if not STRIPE_PRICE_PRO:
551
  raise gr.Error("STRIPE_PRICE_PRO not set")
552
  line_items = [{"price": STRIPE_PRICE_PRO, "quantity": 1}]
553
  mode = "subscription"
554
  meta = {"user_id": str(u["user_id"]), "kind": "pro"}
 
555
  elif kind == "enterprise":
556
  if not STRIPE_PRICE_ENTERPRISE:
557
  raise gr.Error("STRIPE_PRICE_ENTERPRISE not set")
@@ -559,7 +586,6 @@ def create_checkout_session(session_token: str, kind: str) -> str:
559
  mode = "subscription"
560
  meta = {"user_id": str(u["user_id"]), "kind": "enterprise"}
561
 
562
- # Credit packs (one-time)
563
  elif kind in CREDIT_PACKS:
564
  pack = CREDIT_PACKS[kind]
565
  price_id = os.getenv(pack["price_env"], "")
@@ -590,25 +616,28 @@ init_db()
590
 
591
  def chat_turn(session_token: str, agent_id: str, user_msg: str, tool_plan_json: str, history):
592
  u = get_session_user(session_token)
593
- tools = []
 
594
  if tool_plan_json.strip():
595
  try:
596
- tools = json.loads(tool_plan_json)
597
- if not isinstance(tools, list):
598
  raise ValueError("tool_plan must be a JSON list")
599
- tools = [{"tool_id": t["tool_id"], "args": t.get("args", {})} for t in tools]
600
  except Exception as e:
601
  raise gr.Error(f"Bad tool_plan JSON: {e}")
 
602
  if not tools:
603
- tools = [{"tool_id":"echo","args":{"text": user_msg}}]
604
 
605
  res = secure_call(u, agent_id=agent_id, intent="studio_chat_turn", tools=tools)
606
- reply = {"tier": u["tier"], "outputs": res["outputs"], "receipt": res["receipt"], "audit": res["audit"]}
607
  history = history + [(user_msg, json.dumps(reply, indent=2))]
608
  return history, ""
609
 
610
  with gr.Blocks(title=APP_TITLE) as demo:
611
- gr.Markdown(f"# {APP_TITLE}\nPublic users: chat + workflows. Admin: control plane + audit.")
 
612
 
613
  with gr.Tab("Account"):
614
  gr.Markdown("### Signup")
@@ -633,8 +662,11 @@ with gr.Blocks(title=APP_TITLE) as demo:
633
 
634
  with gr.Tab("Studio Chat"):
635
  agent_id = gr.Textbox(label="Agent ID", value="agent_public_01")
636
- tool_plan = gr.Textbox(label="tool_plan (optional JSON list)", lines=6,
637
- value='[{"tool_id":"echo","args":{"text":"Hello from Inner I Studio"}}]')
 
 
 
638
  chat = gr.Chatbot(height=420)
639
  msg = gr.Textbox(label="Message")
640
  send = gr.Button("Send")
@@ -645,76 +677,88 @@ with gr.Blocks(title=APP_TITLE) as demo:
645
 
646
  with gr.Tab("Billing"):
647
  gr.Markdown(
648
- "### Plans"
649
- "- **{PLANS['free']['name']} (Free)**: {PLANS['free']['runs_per_day']} runs/day, low-risk tools"
650
- "- **{PLANS['pro']['name']} (Pro)**: {PLANS['pro']['runs_per_day']} runs/day, medium-risk tools"
651
- "- **{PLANS['enterprise']['name']} (Enterprise)**: {PLANS['enterprise']['runs_per_day']} runs/day, high-risk tools"
652
- "Stripe billing is built-in (optional). If Stripe isn't configured, you can set the Space to Private/Paid as a paywall."
653
- )
654
- pro_btn = gr.Button("Subscribe {PLANS['pro']['name']}")
655
- ent_btn = gr.Button("Subscribe — {PLANS['enterprise']['name']}")
656
- starter_btn = gr.Button("Buy Credits — Starter (250)")
657
- creator_btn = gr.Button("Buy Credits Creator (750)")
658
- power_btn = gr.Button("Buy Credits Power (2500)")
659
- pay_url = gr.Textbox(label="Checkout URL", interactive=False)
660
-
661
- pro_btn.click(lambda s: create_checkout_session(s, "pro"), inputs=[session], outputs=[pay_url])
662
- ent_btn.click(lambda s: create_checkout_session(s, "enterprise"), inputs=[session], outputs=[pay_url])
663
- starter_btn.click(lambda s: create_checkout_session(s, "credits_starter"), inputs=[session], outputs=[pay_url])
664
- creator_btn.click(lambda s: create_checkout_session(s, "credits_creator"), inputs=[session], outputs=[pay_url])
665
- power_btn.click(lambda s: create_checkout_session(s, "credits_power"), inputs=[session], outputs=[pay_url])
666
-
667
- # Admin-only tabs (enforced server-side by require_admin)
 
 
 
 
 
 
668
  with gr.Tab("Admin — Agents"):
669
  gr.Markdown("Admin only.")
670
  a_id = gr.Textbox(label="agent_id", value="agent_builder_01")
671
  a_name = gr.Textbox(label="display_name", value="Inner I Builder Agent")
672
- a_risk = gr.Dropdown(["low","med","high"], value="low", label="risk_tier")
673
- a_ver = gr.Dropdown(["none","basic","full","continuous"], value="basic", label="verification_level")
674
  a_create = gr.Button("Create Agent + Keys")
675
  a_status = gr.Markdown()
676
  a_priv = gr.Textbox(label="Private Key PEM (SAVE SECURELY)", lines=10)
677
  a_pub = gr.Textbox(label="Public Key PEM", lines=8)
678
- a_create.click(create_agent_admin, inputs=[session,a_id,a_name,a_risk,a_ver], outputs=[a_status,a_priv,a_pub])
679
 
680
  a_refresh = gr.Button("Refresh Agents")
681
- a_tbl = gr.Dataframe(headers=["agent_id","display_name","verification","risk","created_at"], interactive=False)
682
  a_refresh.click(list_agents_admin, inputs=[session], outputs=[a_tbl])
683
 
684
  with gr.Tab("Admin — Tools"):
685
  gr.Markdown("Admin only.")
686
  t_refresh = gr.Button("Refresh Tools")
687
- t_tbl = gr.Dataframe(headers=["tool_id","name","risk","enabled","version","description"], interactive=False)
688
  t_refresh.click(list_tools_admin, inputs=[session], outputs=[t_tbl])
689
 
690
  t_id = gr.Textbox(label="tool_id", value="echo")
691
  t_name = gr.Textbox(label="name", value="Echo")
692
  t_desc = gr.Textbox(label="description", value="Returns provided text.")
693
- t_risk = gr.Dropdown(["low","med","high"], value="low", label="risk")
694
  t_enabled = gr.Checkbox(value=True, label="enabled")
695
- t_schema = gr.Textbox(label="json_schema (JSON)", lines=8, value=json.dumps({
696
- "type":"object","properties":{"text":{"type":"string","maxLength":2000}},"required":["text"],"additionalProperties": False
697
- }, indent=2))
 
 
 
 
 
698
  t_save = gr.Button("Save Tool")
699
  t_out = gr.Markdown()
700
- t_save.click(upsert_tool_admin, inputs=[session,t_id,t_name,t_desc,t_risk,t_enabled,t_schema], outputs=[t_out])
701
 
702
  with gr.Tab("Admin — Audit"):
703
  gr.Markdown("Admin only.")
704
  lim = gr.Slider(10, 200, value=50, step=10, label="rows")
705
  aud_btn = gr.Button("Load Audit")
706
- aud_tbl = gr.Dataframe(headers=["id","ts","actor_agent","actor_user","action","prev_hash","row_hash"], interactive=False)
707
  aud_btn.click(read_audit_admin, inputs=[session, lim], outputs=[aud_tbl])
708
 
709
  with gr.Tab("Admin — Users"):
710
  gr.Markdown("Admin only. Emergency tier changes.")
711
  u_email = gr.Textbox(label="User email")
712
- u_tier = gr.Dropdown(["free","pro"], value="pro", label="Set tier")
713
  u_btn = gr.Button("Update tier")
714
  u_out = gr.Markdown()
715
- u_btn.click(admin_set_user_tier, inputs=[session,u_email,u_tier], outputs=[u_out])
716
 
717
- gr.Markdown("---\n**Inner I Principle:** If it can’t be verified, it shouldn’t run.")
 
718
 
719
  # ---------------- FastAPI + Stripe webhook ----------------
720
  app = FastAPI()
@@ -732,11 +776,7 @@ async def stripe_webhook(request: Request):
732
  sig = request.headers.get("stripe-signature", "")
733
 
734
  try:
735
- event = stripe.Webhook.construct_event(
736
- payload=payload,
737
- sig_header=sig,
738
- secret=STRIPE_WEBHOOK_SECRET,
739
- )
740
  except Exception as e:
741
  raise HTTPException(status_code=400, detail=f"bad_signature:{e}")
742
 
@@ -764,27 +804,15 @@ async def stripe_webhook(request: Request):
764
 
765
  if uid:
766
  if customer:
767
- cur.execute(
768
- "UPDATE users SET stripe_customer_id=? WHERE user_id=?",
769
- (customer, uid),
770
- )
771
 
772
  if kind == "pro":
773
- cur.execute(
774
- "UPDATE users SET tier='pro', subscription_status='active' WHERE user_id=?",
775
- (uid,),
776
- )
777
  elif kind == "enterprise":
778
- cur.execute(
779
- "UPDATE users SET tier='enterprise', subscription_status='active' WHERE user_id=?",
780
- (uid,),
781
- )
782
  elif kind in CREDIT_PACKS:
783
  amt = CREDIT_PACKS[kind]["credits"]
784
- cur.execute(
785
- "UPDATE users SET credits=credits+? WHERE user_id=?",
786
- (amt, uid),
787
- )
788
 
789
  conn.commit()
790
 
@@ -794,7 +822,6 @@ async def stripe_webhook(request: Request):
794
 
795
  if customer:
796
  if status in ("active", "trialing"):
797
- # keep enterprise if already, else pro
798
  cur.execute("SELECT tier FROM users WHERE stripe_customer_id=?", (customer,))
799
  row = cur.fetchone()
800
  current = (row["tier"] if row else "free")
@@ -802,19 +829,13 @@ async def stripe_webhook(request: Request):
802
  else:
803
  new_tier = "free"
804
 
805
- cur.execute(
806
- "UPDATE users SET tier=?, subscription_status=? WHERE stripe_customer_id=?",
807
- (new_tier, status, customer),
808
- )
809
  conn.commit()
810
 
811
  elif etype == "customer.subscription.deleted":
812
  customer = data.get("customer")
813
  if customer:
814
- cur.execute(
815
- "UPDATE users SET tier='free', subscription_status='canceled' WHERE stripe_customer_id=?",
816
- (customer,),
817
- )
818
  conn.commit()
819
 
820
  finally:
 
 
1
  import os, json, time, hmac, hashlib, sqlite3, base64, secrets
2
  from typing import Any, Dict, List, Optional, Tuple
3
 
4
  import gradio as gr
5
  from jsonschema import validate, ValidationError
 
6
  import stripe
7
  from fastapi import FastAPI, Request, HTTPException
8
  from fastapi.responses import JSONResponse
 
16
  APP_TITLE = "Inner I Studio — Secure Agent Hub"
17
  DB_PATH = os.getenv("INNERI_DB_PATH", "inneri_studio.db")
18
  RECEIPT_SIGNING_KEY = os.getenv("INNERI_RECEIPT_SIGNING_KEY", "dev_only_change_me")
19
+ JWT_SIGNING_KEY = os.getenv("INNERI_JWT_SIGNING_KEY", "dev_jwt_change_me") # reserved for future API auth
20
  ADMIN_PASSWORD = os.getenv("INNERI_ADMIN_PASSWORD", "admin")
21
 
22
  # Stripe (optional)
 
94
  ).decode("utf-8")
95
  return priv_pem, pub_pem
96
 
97
+ def seed_tools(conn: sqlite3.Connection) -> None:
98
  tools = [
99
  ("echo", "Echo", "Returns the provided text.", "low",
100
+ {"type": "object", "properties": {"text": {"type": "string", "maxLength": 2000}}, "required": ["text"], "additionalProperties": False}),
101
  ("time_now", "Time Now", "Returns server time (UTC).", "low",
102
+ {"type": "object", "properties": {}, "additionalProperties": False}),
103
  ("math_eval", "Math Eval", "Evaluates a simple arithmetic expression.", "med",
104
+ {"type": "object", "properties": {"expression": {"type": "string", "maxLength": 200}}, "required": ["expression"], "additionalProperties": False}),
105
  ]
106
  cur = conn.cursor()
107
  for t in tools:
108
+ cur.execute(
109
+ "INSERT OR IGNORE INTO tools(tool_id,name,description,risk,json_schema,enabled,version) VALUES(?,?,?,?,?,?,?)",
110
+ (t[0], t[1], t[2], t[3], json.dumps(t[4]), 1, 1),
111
+ )
112
  conn.commit()
113
 
114
+ def init_db() -> None:
115
  conn = db()
116
  cur = conn.cursor()
117
  cur.executescript("""
 
184
  cur.execute("SELECT user_id FROM users WHERE email=?", ("admin@local",))
185
  if not cur.fetchone():
186
  ph = sha256_hex("admin@local:" + ADMIN_PASSWORD)
187
+ cur.execute(
188
+ "INSERT INTO users(email,password_hash,role,tier,credits,created_at) VALUES(?,?,?,?,?,?)",
189
+ ("admin@local", ph, "admin", "pro", 0, now_unix()),
190
+ )
191
  conn.commit()
192
 
193
  # seed tools
 
199
  cur.execute("SELECT agent_id FROM agents WHERE agent_id='agent_public_01'")
200
  if not cur.fetchone():
201
  _, pub = gen_ed25519_keypair()
202
+ cur.execute(
203
+ "INSERT INTO agents(agent_id,display_name,verification_level,risk_tier,public_key_pem,created_at) VALUES(?,?,?,?,?,?)",
204
+ ("agent_public_01", "Inner I Public Agent", "none", "low", pub, now_unix()),
205
+ )
206
+ cur.execute(
207
+ "INSERT OR IGNORE INTO reputations(agent_id,score,updated_at) VALUES(?,?,?)",
208
+ ("agent_public_01", 50, now_unix()),
209
+ )
210
  conn.commit()
211
 
212
  conn.close()
213
 
214
+ def audit_append(
215
+ actor_agent_id: Optional[str],
216
+ actor_user_id: Optional[int],
217
+ action: str,
218
+ request_obj: Dict[str, Any],
219
+ result_obj: Dict[str, Any],
220
+ ) -> Dict[str, Any]:
221
  conn = db()
222
  cur = conn.cursor()
223
  cur.execute("SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1")
224
  row = cur.fetchone()
225
  prev_hash = row["row_hash"] if row else None
226
+ row_obj = {
227
+ "actor_agent_id": actor_agent_id,
228
+ "actor_user_id": actor_user_id,
229
+ "action": action,
230
+ "request": request_obj,
231
+ "result": result_obj,
232
+ "prev_hash": prev_hash,
233
+ }
234
  row_hash = sha256_hex(canonical_json(row_obj))
235
+ cur.execute(
236
+ "INSERT INTO audit_log(ts,actor_agent_id,actor_user_id,action,request_json,result_json,prev_hash,row_hash) VALUES(?,?,?,?,?,?,?,?)",
237
+ (now_unix(), actor_agent_id, actor_user_id, action, json.dumps(request_obj), json.dumps(result_obj), prev_hash, row_hash),
238
+ )
239
  conn.commit()
240
  audit_id = cur.lastrowid
241
  conn.close()
 
251
  raise gr.Error("Enter a valid email")
252
  if len(password) < 8:
253
  raise gr.Error("Password must be 8+ chars")
254
+
255
  conn = db()
256
  cur = conn.cursor()
257
  try:
258
+ cur.execute(
259
+ "INSERT INTO users(email,password_hash,role,tier,credits,created_at) VALUES(?,?,?,?,?,?)",
260
+ (email, hash_pw(email, password), "user", "free", 0, now_unix()),
261
+ )
262
  conn.commit()
263
  except sqlite3.IntegrityError:
264
  conn.close()
 
273
  cur.execute("SELECT user_id,password_hash FROM users WHERE email=?", (email,))
274
  row = cur.fetchone()
275
  conn.close()
276
+
277
  if not row:
278
  return "", "❌ user_not_found"
279
  if hash_pw(email, password) != row["password_hash"]:
280
  return "", "❌ bad_password"
281
+
282
  token = b64url(secrets.token_bytes(24))
283
  now = now_unix()
284
  exp = now + 3600 * 8
285
+
286
  conn = db()
287
  cur = conn.cursor()
288
+ cur.execute(
289
+ "INSERT INTO sessions(token,user_id,created_at,expires_at) VALUES(?,?,?,?)",
290
+ (token, row["user_id"], now, exp),
291
+ )
292
  conn.commit()
293
  conn.close()
294
  return token, "✅ logged_in"
 
296
  def get_session_user(session_token: str) -> Dict[str, Any]:
297
  conn = db()
298
  cur = conn.cursor()
299
+ cur.execute(
300
+ """
301
  SELECT u.user_id,u.email,u.role,u.tier,u.credits,u.subscription_status,s.expires_at
302
  FROM sessions s JOIN users u ON u.user_id=s.user_id
303
  WHERE s.token=?
304
+ """,
305
+ (session_token,),
306
+ )
307
  row = cur.fetchone()
308
  conn.close()
309
  if not row:
 
328
 
329
  conn = db()
330
  cur = conn.cursor()
331
+
332
  cur.execute("INSERT OR IGNORE INTO usage_daily(user_id,day,runs) VALUES(?,?,0)", (user["user_id"], day))
333
  cur.execute("SELECT runs FROM usage_daily WHERE user_id=? AND day=?", (user["user_id"], day))
334
  runs = int(cur.fetchone()["runs"])
335
 
 
336
  if runs >= limit and user["role"] != "admin":
337
+ # Free tier may continue using credits (burst mode)
338
  if user["tier"] == "free" and user["credits"] >= RUN_CREDIT_COST:
339
+ cur.execute(
340
+ "UPDATE users SET credits=credits-? WHERE user_id=? AND credits>=?",
341
+ (RUN_CREDIT_COST, user["user_id"], RUN_CREDIT_COST),
342
+ )
343
  if cur.rowcount != 1:
344
  conn.close()
345
  raise gr.Error(f"Daily limit reached ({limit}). Upgrade to {PLANS['pro']['name']} or buy credits.")
 
350
  cur.execute("UPDATE usage_daily SET runs=runs+1 WHERE user_id=? AND day=?", (user["user_id"], day))
351
  conn.commit()
352
  conn.close()
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  # ---------------- Policy + tools ----------------
355
  def policy_decide(agent: Dict[str, Any], request: Dict[str, Any]) -> Dict[str, Any]:
356
  if "secret" in (request.get("data_scopes") or []):
357
  return {"allow": False, "mode": "deny", "reasons": ["secret_scope_blocked"]}
358
+
359
  tools = request.get("tools") or []
360
  if agent.get("risk_tier") == "high" or any(t.get("risk") == "high" for t in tools):
361
  return {"allow": False, "mode": "deny", "reasons": ["high_risk_blocked"]}
362
+
363
  if agent.get("verification_level") == "none" or any(t.get("risk") == "med" for t in tools):
364
  return {"allow": True, "mode": "sandbox", "reasons": ["sandbox_unverified_or_med"]}
365
+
366
  return {"allow": True, "mode": "normal", "reasons": []}
367
 
368
+ _OPS = {
369
+ ast.Add: op.add,
370
+ ast.Sub: op.sub,
371
+ ast.Mult: op.mul,
372
+ ast.Div: op.truediv,
373
+ ast.Pow: op.pow,
374
+ ast.USub: op.neg,
375
+ ast.Mod: op.mod,
376
+ ast.FloorDiv: op.floordiv,
377
+ }
378
+
379
  def _eval(node):
380
  if isinstance(node, ast.Num):
381
  return node.n
 
397
 
398
  def secure_call(user: Dict[str, Any], agent_id: str, intent: str, tools: List[Dict[str, Any]]) -> Dict[str, Any]:
399
  check_and_increment_run(user)
400
+
401
  conn = db()
402
  cur = conn.cursor()
403
+
404
  cur.execute("SELECT * FROM agents WHERE agent_id=?", (agent_id,))
405
  a = cur.fetchone()
406
  if not a:
 
408
  raise gr.Error("agent_not_found")
409
  agent = dict(a)
410
 
411
+ tools_meta: List[Dict[str, Any]] = []
412
  for tc in tools:
413
  cur.execute("SELECT * FROM tools WHERE tool_id=?", (tc["tool_id"],))
414
  t = cur.fetchone()
 
417
  raise gr.Error(f"tool_not_found_or_disabled: {tc['tool_id']}")
418
  tools_meta.append({"tool_id": t["tool_id"], "risk": t["risk"], "schema": json.loads(t["json_schema"])})
419
 
420
+ decision = policy_decide(
421
+ agent,
422
+ {"intent": intent, "tools": [{"tool_id": tm["tool_id"], "risk": tm["risk"]} for tm in tools_meta], "data_scopes": ["public"]},
423
+ )
424
  if not decision["allow"]:
425
  conn.close()
426
  audit_append(agent_id, user["user_id"], "secure_call.deny", {"intent": intent, "tools": tools}, {"decision": decision})
427
  return {"denied": True, "decision": decision}
428
 
429
  mode = decision["mode"]
430
+ outputs: List[Dict[str, Any]] = []
431
+
432
+ # Tier-based tool risk gating
433
+ tier = user["tier"] if user["role"] != "admin" else "enterprise"
434
+ if tier not in PLANS:
435
+ tier = "free"
436
+ tier_max = PLANS[tier]["max_tool_risk"]
437
+ risk_rank = {"low": 1, "med": 2, "high": 3}
438
+ tier_max_rank = risk_rank[tier_max]
439
+
440
+ for tc, tm in zip(tools, tools_meta):
441
  try:
442
  validate(instance=tc.get("args", {}), schema=tm["schema"])
443
  except ValidationError:
444
  conn.close()
445
  raise gr.Error(f"args_schema_invalid: {tm['tool_id']}")
446
 
447
+ if risk_rank.get(tm["risk"], 3) > tier_max_rank:
448
+ outputs.append({"tool_id": tm["tool_id"], "blocked": True, "reason": f"plan_blocks_{tm['risk']}_risk"})
449
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
+ if mode == "sandbox" and tm["risk"] != "low":
452
+ outputs.append({"tool_id": tm["tool_id"], "blocked": True, "reason": "sandbox_mode"})
453
+ continue
454
+
455
+ try:
456
+ outputs.append({"tool_id": tm["tool_id"], "output": run_tool(tm["tool_id"], tc.get("args", {}))})
457
+ except Exception as e:
458
+ outputs.append({"tool_id": tm["tool_id"], "error": str(e)})
459
+
460
+ receipt = {
461
+ "ts_unix": now_unix(),
462
+ "agent_id": agent_id,
463
+ "user_id": user["user_id"],
464
+ "intent": intent,
465
+ "mode": mode,
466
+ "decision": decision,
467
+ "outputs_hash": sha256_hex(canonical_json(outputs)),
468
+ }
469
+ receipt["signature"] = sign_hmac(receipt, RECEIPT_SIGNING_KEY)
470
+ audit = audit_append(agent_id, user["user_id"], "secure_call.run", {"intent": intent, "tools": tools}, {"outputs": outputs, "receipt": receipt})
471
+
472
+ conn.close()
473
+ return {"outputs": outputs, "receipt": receipt, "audit": audit}
 
 
 
 
 
 
 
 
 
474
 
475
  # ---------------- Admin actions ----------------
476
  def create_agent_admin(session_token: str, agent_id: str, display_name: str, risk_tier: str, verification_level: str):
 
478
  priv_pem, pub_pem = gen_ed25519_keypair()
479
  conn = db()
480
  cur = conn.cursor()
481
+ cur.execute(
482
+ "INSERT INTO agents(agent_id,display_name,verification_level,risk_tier,public_key_pem,created_at) VALUES(?,?,?,?,?,?)",
483
+ (agent_id, display_name, verification_level, risk_tier, pub_pem, now_unix()),
484
+ )
485
  conn.commit()
486
  conn.close()
487
  return "✅ agent_created (save private key)", priv_pem, pub_pem
 
502
  cur.execute("SELECT tool_id,name,risk,enabled,version,description FROM tools ORDER BY tool_id")
503
  rows = cur.fetchall()
504
  conn.close()
505
+ return [[r["tool_id"], r["name"], r["risk"], "yes" if int(r["enabled"]) == 1 else "no", str(r["version"]), r["description"]] for r in rows]
506
 
507
  def upsert_tool_admin(session_token: str, tool_id: str, name: str, description: str, risk: str, enabled: bool, schema_json: str):
508
  require_admin(session_token)
 
512
  raise ValueError("schema.type must be object")
513
  except Exception as e:
514
  raise gr.Error(f"Invalid schema JSON: {e}")
515
+
516
  conn = db()
517
  cur = conn.cursor()
518
  cur.execute("SELECT version FROM tools WHERE tool_id=?", (tool_id,))
519
  row = cur.fetchone()
520
  version = (int(row["version"]) + 1) if row else 1
521
+
522
+ cur.execute(
523
+ """
524
  INSERT INTO tools(tool_id,name,description,risk,json_schema,enabled,version)
525
  VALUES(?,?,?,?,?,?,?)
526
  ON CONFLICT(tool_id) DO UPDATE SET
 
530
  json_schema=excluded.json_schema,
531
  enabled=excluded.enabled,
532
  version=?
533
+ """,
534
+ (tool_id, name, description, risk, json.dumps(schema), 1 if enabled else 0, version, version),
535
+ )
536
  conn.commit()
537
  conn.close()
538
  return f"✅ saved_tool `{tool_id}` v{version}"
 
549
  def admin_set_user_tier(session_token: str, email: str, tier: str) -> str:
550
  require_admin(session_token)
551
  email = email.strip().lower()
552
+ if tier not in ("free", "pro", "enterprise"):
553
+ raise gr.Error("tier must be free, pro, or enterprise")
554
  conn = db()
555
  cur = conn.cursor()
556
  cur.execute("UPDATE users SET tier=? WHERE email=?", (tier, email))
 
562
  # ---------------- Billing ----------------
563
  def user_me(session_token: str) -> str:
564
  u = get_session_user(session_token)
565
+ safe = {k: u[k] for k in ["user_id", "email", "role", "tier", "credits", "subscription_status"]}
566
+ safe["plan_name"] = PLANS.get(safe["tier"], PLANS["free"])["name"] if safe["role"] != "admin" else PLANS["enterprise"]["name"]
567
+ safe["stripe_enabled"] = STRIPE_ENABLED
568
+ return json.dumps(safe, indent=2)
569
 
570
  def create_checkout_session(session_token: str, kind: str) -> str:
571
  if not STRIPE_ENABLED:
572
  raise gr.Error("Stripe not configured (set STRIPE_SECRET_KEY + PUBLIC_BASE_URL).")
573
  u = get_session_user(session_token)
574
 
 
575
  if kind == "pro":
576
  if not STRIPE_PRICE_PRO:
577
  raise gr.Error("STRIPE_PRICE_PRO not set")
578
  line_items = [{"price": STRIPE_PRICE_PRO, "quantity": 1}]
579
  mode = "subscription"
580
  meta = {"user_id": str(u["user_id"]), "kind": "pro"}
581
+
582
  elif kind == "enterprise":
583
  if not STRIPE_PRICE_ENTERPRISE:
584
  raise gr.Error("STRIPE_PRICE_ENTERPRISE not set")
 
586
  mode = "subscription"
587
  meta = {"user_id": str(u["user_id"]), "kind": "enterprise"}
588
 
 
589
  elif kind in CREDIT_PACKS:
590
  pack = CREDIT_PACKS[kind]
591
  price_id = os.getenv(pack["price_env"], "")
 
616
 
617
  def chat_turn(session_token: str, agent_id: str, user_msg: str, tool_plan_json: str, history):
618
  u = get_session_user(session_token)
619
+
620
+ tools: List[Dict[str, Any]] = []
621
  if tool_plan_json.strip():
622
  try:
623
+ tool_plan = json.loads(tool_plan_json)
624
+ if not isinstance(tool_plan, list):
625
  raise ValueError("tool_plan must be a JSON list")
626
+ tools = [{"tool_id": t["tool_id"], "args": t.get("args", {})} for t in tool_plan]
627
  except Exception as e:
628
  raise gr.Error(f"Bad tool_plan JSON: {e}")
629
+
630
  if not tools:
631
+ tools = [{"tool_id": "echo", "args": {"text": user_msg}}]
632
 
633
  res = secure_call(u, agent_id=agent_id, intent="studio_chat_turn", tools=tools)
634
+ reply = {"tier": u["tier"], "outputs": res.get("outputs"), "receipt": res.get("receipt"), "audit": res.get("audit")}
635
  history = history + [(user_msg, json.dumps(reply, indent=2))]
636
  return history, ""
637
 
638
  with gr.Blocks(title=APP_TITLE) as demo:
639
+ gr.Markdown(f"# {APP_TITLE}
640
+ Public users: chat + workflows. Admin: control plane + audit.")
641
 
642
  with gr.Tab("Account"):
643
  gr.Markdown("### Signup")
 
662
 
663
  with gr.Tab("Studio Chat"):
664
  agent_id = gr.Textbox(label="Agent ID", value="agent_public_01")
665
+ tool_plan = gr.Textbox(
666
+ label="tool_plan (optional JSON list)",
667
+ lines=6,
668
+ value='[{"tool_id":"echo","args":{"text":"Hello from Inner I Studio"}}]',
669
+ )
670
  chat = gr.Chatbot(height=420)
671
  msg = gr.Textbox(label="Message")
672
  send = gr.Button("Send")
 
677
 
678
  with gr.Tab("Billing"):
679
  gr.Markdown(
680
+ "### Plans
681
+ "
682
+ "- **Observer (Free)**: 25 runs/day, low-risk tools
683
+ "
684
+ "- **Builder (Pro)**: 500 runs/day, medium-risk tools
685
+ "
686
+ "- **Sovereign (Enterprise)**: 5000 runs/day, high-risk tools
687
+
688
+ "
689
+ "Stripe billing is built-in (optional). If Stripe isn't configured, "
690
+ "you can set the Space to Private/Paid as a paywall."
691
+ )
692
+
693
+ pro_btn = gr.Button("Subscribe Builder")
694
+ ent_btn = gr.Button("Subscribe Sovereign")
695
+ starter_btn = gr.Button("Buy Credits — Starter (250)")
696
+ creator_btn = gr.Button("Buy Credits — Creator (750)")
697
+ power_btn = gr.Button("Buy Credits — Power (2500)")
698
+ pay_url = gr.Textbox(label="Checkout URL", interactive=False)
699
+
700
+ pro_btn.click(lambda s: create_checkout_session(s, "pro"), inputs=[session], outputs=[pay_url])
701
+ ent_btn.click(lambda s: create_checkout_session(s, "enterprise"), inputs=[session], outputs=[pay_url])
702
+ starter_btn.click(lambda s: create_checkout_session(s, "credits_starter"), inputs=[session], outputs=[pay_url])
703
+ creator_btn.click(lambda s: create_checkout_session(s, "credits_creator"), inputs=[session], outputs=[pay_url])
704
+ power_btn.click(lambda s: create_checkout_session(s, "credits_power"), inputs=[session], outputs=[pay_url])
705
+
706
  with gr.Tab("Admin — Agents"):
707
  gr.Markdown("Admin only.")
708
  a_id = gr.Textbox(label="agent_id", value="agent_builder_01")
709
  a_name = gr.Textbox(label="display_name", value="Inner I Builder Agent")
710
+ a_risk = gr.Dropdown(["low", "med", "high"], value="low", label="risk_tier")
711
+ a_ver = gr.Dropdown(["none", "basic", "full", "continuous"], value="basic", label="verification_level")
712
  a_create = gr.Button("Create Agent + Keys")
713
  a_status = gr.Markdown()
714
  a_priv = gr.Textbox(label="Private Key PEM (SAVE SECURELY)", lines=10)
715
  a_pub = gr.Textbox(label="Public Key PEM", lines=8)
716
+ a_create.click(create_agent_admin, inputs=[session, a_id, a_name, a_risk, a_ver], outputs=[a_status, a_priv, a_pub])
717
 
718
  a_refresh = gr.Button("Refresh Agents")
719
+ a_tbl = gr.Dataframe(headers=["agent_id", "display_name", "verification", "risk", "created_at"], interactive=False)
720
  a_refresh.click(list_agents_admin, inputs=[session], outputs=[a_tbl])
721
 
722
  with gr.Tab("Admin — Tools"):
723
  gr.Markdown("Admin only.")
724
  t_refresh = gr.Button("Refresh Tools")
725
+ t_tbl = gr.Dataframe(headers=["tool_id", "name", "risk", "enabled", "version", "description"], interactive=False)
726
  t_refresh.click(list_tools_admin, inputs=[session], outputs=[t_tbl])
727
 
728
  t_id = gr.Textbox(label="tool_id", value="echo")
729
  t_name = gr.Textbox(label="name", value="Echo")
730
  t_desc = gr.Textbox(label="description", value="Returns provided text.")
731
+ t_risk = gr.Dropdown(["low", "med", "high"], value="low", label="risk")
732
  t_enabled = gr.Checkbox(value=True, label="enabled")
733
+ t_schema = gr.Textbox(
734
+ label="json_schema (JSON)",
735
+ lines=8,
736
+ value=json.dumps(
737
+ {"type": "object", "properties": {"text": {"type": "string", "maxLength": 2000}}, "required": ["text"], "additionalProperties": False},
738
+ indent=2,
739
+ ),
740
+ )
741
  t_save = gr.Button("Save Tool")
742
  t_out = gr.Markdown()
743
+ t_save.click(upsert_tool_admin, inputs=[session, t_id, t_name, t_desc, t_risk, t_enabled, t_schema], outputs=[t_out])
744
 
745
  with gr.Tab("Admin — Audit"):
746
  gr.Markdown("Admin only.")
747
  lim = gr.Slider(10, 200, value=50, step=10, label="rows")
748
  aud_btn = gr.Button("Load Audit")
749
+ aud_tbl = gr.Dataframe(headers=["id", "ts", "actor_agent", "actor_user", "action", "prev_hash", "row_hash"], interactive=False)
750
  aud_btn.click(read_audit_admin, inputs=[session, lim], outputs=[aud_tbl])
751
 
752
  with gr.Tab("Admin — Users"):
753
  gr.Markdown("Admin only. Emergency tier changes.")
754
  u_email = gr.Textbox(label="User email")
755
+ u_tier = gr.Dropdown(["free", "pro", "enterprise"], value="pro", label="Set tier")
756
  u_btn = gr.Button("Update tier")
757
  u_out = gr.Markdown()
758
+ u_btn.click(admin_set_user_tier, inputs=[session, u_email, u_tier], outputs=[u_out])
759
 
760
+ gr.Markdown("---
761
+ **Inner I Principle:** If it can’t be verified, it shouldn’t run.")
762
 
763
  # ---------------- FastAPI + Stripe webhook ----------------
764
  app = FastAPI()
 
776
  sig = request.headers.get("stripe-signature", "")
777
 
778
  try:
779
+ event = stripe.Webhook.construct_event(payload=payload, sig_header=sig, secret=STRIPE_WEBHOOK_SECRET)
 
 
 
 
780
  except Exception as e:
781
  raise HTTPException(status_code=400, detail=f"bad_signature:{e}")
782
 
 
804
 
805
  if uid:
806
  if customer:
807
+ cur.execute("UPDATE users SET stripe_customer_id=? WHERE user_id=?", (customer, uid))
 
 
 
808
 
809
  if kind == "pro":
810
+ cur.execute("UPDATE users SET tier='pro', subscription_status='active' WHERE user_id=?", (uid,))
 
 
 
811
  elif kind == "enterprise":
812
+ cur.execute("UPDATE users SET tier='enterprise', subscription_status='active' WHERE user_id=?", (uid,))
 
 
 
813
  elif kind in CREDIT_PACKS:
814
  amt = CREDIT_PACKS[kind]["credits"]
815
+ cur.execute("UPDATE users SET credits=credits+? WHERE user_id=?", (amt, uid))
 
 
 
816
 
817
  conn.commit()
818
 
 
822
 
823
  if customer:
824
  if status in ("active", "trialing"):
 
825
  cur.execute("SELECT tier FROM users WHERE stripe_customer_id=?", (customer,))
826
  row = cur.fetchone()
827
  current = (row["tier"] if row else "free")
 
829
  else:
830
  new_tier = "free"
831
 
832
+ cur.execute("UPDATE users SET tier=?, subscription_status=? WHERE stripe_customer_id=?", (new_tier, status, customer))
 
 
 
833
  conn.commit()
834
 
835
  elif etype == "customer.subscription.deleted":
836
  customer = data.get("customer")
837
  if customer:
838
+ cur.execute("UPDATE users SET tier='free', subscription_status='canceled' WHERE stripe_customer_id=?", (customer,))
 
 
 
839
  conn.commit()
840
 
841
  finally: