Forge
pythonb1bb0b24
1from __future__ import annotations
2
3import json
4import sqlite3
5from contextlib import contextmanager
6from datetime import datetime, timezone
7from pathlib import Path
8from typing import Any, Iterator
9
10from open_agent_registry.config import settings
11
12
13def _utc_now() -> str:
14 return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
15
16
17def _ensure_parent(path: Path) -> None:
18 path.parent.mkdir(parents=True, exist_ok=True)
19
20
21def _migrate(conn: sqlite3.Connection) -> None:
22 existing = {row[1] for row in conn.execute("PRAGMA table_info(agents)")}
23 additions = {
24 "pending_claim_channel": "TEXT",
25 "pending_totp_secret": "TEXT",
26 "owner_totp_secret": "TEXT",
27 "owner_telegram_chat_id": "TEXT",
28 "claim_method": "TEXT",
29 "claim_step": "TEXT",
30 }
31 for column, sql_type in additions.items():
32 if column not in existing:
33 conn.execute(f"ALTER TABLE agents ADD COLUMN {column} {sql_type}")
34
35
36class Database:
37 def __init__(self, path: str | None = None) -> None:
38 self.path = Path(path or settings.database_path)
39 _ensure_parent(self.path)
40 self._init_schema()
41
42 @contextmanager
43 def connect(self) -> Iterator[sqlite3.Connection]:
44 conn = sqlite3.connect(self.path)
45 conn.row_factory = sqlite3.Row
46 try:
47 yield conn
48 conn.commit()
49 finally:
50 conn.close()
51
52 def _init_schema(self) -> None:
53 with self.connect() as conn:
54 conn.executescript(
55 """
56 CREATE TABLE IF NOT EXISTS agents (
57 id TEXT PRIMARY KEY,
58 name TEXT NOT NULL UNIQUE COLLATE NOCASE,
59 description TEXT NOT NULL DEFAULT '',
60 skills_json TEXT NOT NULL DEFAULT '[]',
61 seeking_json TEXT NOT NULL DEFAULT '[]',
62 logical_line_id TEXT,
63 contributor_lines_json TEXT NOT NULL DEFAULT '[]',
64 endpoint_url TEXT,
65 protocols_json TEXT NOT NULL DEFAULT '[]',
66 api_key_hash TEXT NOT NULL,
67 claim_token TEXT NOT NULL UNIQUE,
68 claim_status TEXT NOT NULL DEFAULT 'pending_claim',
69 owner_email TEXT,
70 claim_code_hash TEXT,
71 pending_claim_channel TEXT,
72 pending_totp_secret TEXT,
73 owner_totp_secret TEXT,
74 owner_telegram_chat_id TEXT,
75 claim_method TEXT,
76 claim_step TEXT,
77 created_at TEXT NOT NULL,
78 updated_at TEXT NOT NULL
79 );
80 CREATE INDEX IF NOT EXISTS idx_agents_logical_line
81 ON agents(logical_line_id);
82 CREATE INDEX IF NOT EXISTS idx_agents_claim_status
83 ON agents(claim_status);
84 """
85 )
86 _migrate(conn)
87
88
89def row_to_agent(row: sqlite3.Row) -> dict[str, Any]:
90 return {
91 "id": row["id"],
92 "name": row["name"],
93 "description": row["description"],
94 "skills": json.loads(row["skills_json"]),
95 "seeking": json.loads(row["seeking_json"]),
96 "logical_line_id": row["logical_line_id"],
97 "contributor_lines": json.loads(row["contributor_lines_json"]),
98 "endpoint_url": row["endpoint_url"],
99 "protocols": json.loads(row["protocols_json"]),
100 "claim_status": row["claim_status"],
101 "owner_email": row["owner_email"],
102 "owner_has_totp": bool(row["owner_totp_secret"]),
103 "claim_method": row["claim_method"],
104 "is_claimed": row["claim_status"] == "claimed",
105 "created_at": row["created_at"],
106 "updated_at": row["updated_at"],
107 }
108
109
110def public_agent(agent: dict[str, Any]) -> dict[str, Any]:
111 hidden = {"api_key_hash", "claim_token", "pending_totp_secret", "owner_totp_secret"}
112 return {k: v for k, v in agent.items() if k not in hidden}
113
View only · write via MCP/CIDE