haldir 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haldir-0.1.0/PKG-INFO +98 -0
- haldir-0.1.0/README.md +81 -0
- haldir-0.1.0/haldir_db.py +289 -0
- haldir-0.1.0/haldir_gate/__init__.py +9 -0
- haldir-0.1.0/haldir_gate/approvals.py +357 -0
- haldir-0.1.0/haldir_gate/gate.py +231 -0
- haldir-0.1.0/haldir_vault/__init__.py +10 -0
- haldir-0.1.0/haldir_vault/vault.py +182 -0
- haldir-0.1.0/haldir_watch/__init__.py +10 -0
- haldir-0.1.0/haldir_watch/watch.py +214 -0
- haldir-0.1.0/haldir_watch/webhooks.py +194 -0
- haldir-0.1.0/mcp_server.py +274 -0
- haldir-0.1.0/pyproject.toml +43 -0
- haldir-0.1.0/sdk/__init__.py +23 -0
- haldir-0.1.0/sdk/client.py +447 -0
haldir-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haldir
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The guardian layer for AI agents — identity, secrets, audit via MCP
|
|
5
|
+
Project-URL: Homepage, https://haldir.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/ExposureGuard/haldir
|
|
7
|
+
Project-URL: Documentation, https://haldir.xyz
|
|
8
|
+
Author-email: Sterling <sterling@getexposureguard.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai-agents,audit,compliance,governance,identity,mcp,model-context-protocol,secrets,security
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: cryptography>=42.0.0
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: mcp>=1.0.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
<!-- mcp-name: io.github.ExposureGuard/haldir -->
|
|
19
|
+
# Haldir
|
|
20
|
+
|
|
21
|
+
**The guardian layer for AI agents.**
|
|
22
|
+
|
|
23
|
+
Haldir is an MCP server platform that gives AI agents identity, security, and accountability. Every agent action — browsing, paying, authenticating, calling APIs — flows through Haldir.
|
|
24
|
+
|
|
25
|
+
## Products
|
|
26
|
+
|
|
27
|
+
| Product | What it does | MCP Tools |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Haldir Gate** | Agent identity, auth, permissions | `authenticate`, `check_permission`, `create_session`, `revoke_session` |
|
|
30
|
+
| **Haldir Vault** | Secrets, credentials, payment limits | `get_secret`, `store_secret`, `authorize_payment`, `check_budget` |
|
|
31
|
+
| **Haldir Watch** | Audit logs, compliance, cost tracking | `log_action`, `get_audit_trail`, `get_spend`, `flag_anomaly` |
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install haldir
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from haldir import Gate, Vault, Watch
|
|
41
|
+
|
|
42
|
+
# Initialize
|
|
43
|
+
gate = Gate(api_key="your-key")
|
|
44
|
+
vault = Vault(api_key="your-key")
|
|
45
|
+
watch = Watch(api_key="your-key")
|
|
46
|
+
|
|
47
|
+
# Authenticate an agent
|
|
48
|
+
session = gate.create_session(agent_id="my-agent", scopes=["read", "spend:50"])
|
|
49
|
+
|
|
50
|
+
# Get a secret safely
|
|
51
|
+
api_key = vault.get_secret("stripe_key", session=session)
|
|
52
|
+
|
|
53
|
+
# Every action is logged
|
|
54
|
+
watch.log_action(session=session, tool="stripe", action="charge", amount=29.99)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## MCP Server
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"haldir": {
|
|
63
|
+
"command": "haldir-mcp",
|
|
64
|
+
"env": {
|
|
65
|
+
"HALDIR_API_KEY": "your-key"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Agent (Claude, GPT, etc.)
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
┌─────────────────────────┐
|
|
79
|
+
│ Haldir Gate │ ← Identity + permissions
|
|
80
|
+
│ "Can this agent do X?" │
|
|
81
|
+
└────────┬────────────────┘
|
|
82
|
+
│
|
|
83
|
+
┌────┴────┐
|
|
84
|
+
▼ ▼
|
|
85
|
+
┌────────┐ ┌────────┐
|
|
86
|
+
│ Vault │ │ Watch │
|
|
87
|
+
│secrets │ │ audit │
|
|
88
|
+
│payments│ │ costs │
|
|
89
|
+
└────────┘ └────────┘
|
|
90
|
+
│ │
|
|
91
|
+
▼ ▼
|
|
92
|
+
External Storage
|
|
93
|
+
APIs (Postgres)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
haldir-0.1.0/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.ExposureGuard/haldir -->
|
|
2
|
+
# Haldir
|
|
3
|
+
|
|
4
|
+
**The guardian layer for AI agents.**
|
|
5
|
+
|
|
6
|
+
Haldir is an MCP server platform that gives AI agents identity, security, and accountability. Every agent action — browsing, paying, authenticating, calling APIs — flows through Haldir.
|
|
7
|
+
|
|
8
|
+
## Products
|
|
9
|
+
|
|
10
|
+
| Product | What it does | MCP Tools |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Haldir Gate** | Agent identity, auth, permissions | `authenticate`, `check_permission`, `create_session`, `revoke_session` |
|
|
13
|
+
| **Haldir Vault** | Secrets, credentials, payment limits | `get_secret`, `store_secret`, `authorize_payment`, `check_budget` |
|
|
14
|
+
| **Haldir Watch** | Audit logs, compliance, cost tracking | `log_action`, `get_audit_trail`, `get_spend`, `flag_anomaly` |
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install haldir
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from haldir import Gate, Vault, Watch
|
|
24
|
+
|
|
25
|
+
# Initialize
|
|
26
|
+
gate = Gate(api_key="your-key")
|
|
27
|
+
vault = Vault(api_key="your-key")
|
|
28
|
+
watch = Watch(api_key="your-key")
|
|
29
|
+
|
|
30
|
+
# Authenticate an agent
|
|
31
|
+
session = gate.create_session(agent_id="my-agent", scopes=["read", "spend:50"])
|
|
32
|
+
|
|
33
|
+
# Get a secret safely
|
|
34
|
+
api_key = vault.get_secret("stripe_key", session=session)
|
|
35
|
+
|
|
36
|
+
# Every action is logged
|
|
37
|
+
watch.log_action(session=session, tool="stripe", action="charge", amount=29.99)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## MCP Server
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"haldir": {
|
|
46
|
+
"command": "haldir-mcp",
|
|
47
|
+
"env": {
|
|
48
|
+
"HALDIR_API_KEY": "your-key"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Architecture
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Agent (Claude, GPT, etc.)
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
┌─────────────────────────┐
|
|
62
|
+
│ Haldir Gate │ ← Identity + permissions
|
|
63
|
+
│ "Can this agent do X?" │
|
|
64
|
+
└────────┬────────────────┘
|
|
65
|
+
│
|
|
66
|
+
┌────┴────┐
|
|
67
|
+
▼ ▼
|
|
68
|
+
┌────────┐ ┌────────┐
|
|
69
|
+
│ Vault │ │ Watch │
|
|
70
|
+
│secrets │ │ audit │
|
|
71
|
+
│payments│ │ costs │
|
|
72
|
+
└────────┘ └────────┘
|
|
73
|
+
│ │
|
|
74
|
+
▼ ▼
|
|
75
|
+
External Storage
|
|
76
|
+
APIs (Postgres)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Haldir Database — Multi-tenant persistence layer.
|
|
3
|
+
|
|
4
|
+
Supports SQLite (dev/small deployments) and PostgreSQL (production).
|
|
5
|
+
Every table has a tenant_id column for data isolation between API keys.
|
|
6
|
+
|
|
7
|
+
Set DATABASE_URL for Postgres: postgresql://user:pass@host/haldir
|
|
8
|
+
Otherwise falls back to SQLite at HALDIR_DB_PATH.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import sqlite3
|
|
14
|
+
|
|
15
|
+
DATABASE_URL = os.environ.get("DATABASE_URL", "")
|
|
16
|
+
DEFAULT_DB_PATH = os.environ.get("HALDIR_DB_PATH", "/data/haldir.db" if os.path.isdir("/data") else "haldir.db")
|
|
17
|
+
|
|
18
|
+
_pg_pool = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_postgres() -> bool:
|
|
22
|
+
return DATABASE_URL.startswith("postgres")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_db(db_path: str = DEFAULT_DB_PATH):
|
|
26
|
+
"""Get a database connection. Returns SQLite or Postgres depending on config."""
|
|
27
|
+
if _is_postgres():
|
|
28
|
+
return _get_pg()
|
|
29
|
+
conn = sqlite3.connect(db_path, timeout=10)
|
|
30
|
+
conn.row_factory = sqlite3.Row
|
|
31
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
32
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
33
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
34
|
+
return conn
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_pg():
|
|
38
|
+
"""Get a PostgreSQL connection from the pool."""
|
|
39
|
+
global _pg_pool
|
|
40
|
+
import psycopg2
|
|
41
|
+
import psycopg2.extras
|
|
42
|
+
|
|
43
|
+
if _pg_pool is None:
|
|
44
|
+
from psycopg2 import pool
|
|
45
|
+
_pg_pool = pool.ThreadedConnectionPool(1, 10, DATABASE_URL)
|
|
46
|
+
|
|
47
|
+
conn = _pg_pool.getconn()
|
|
48
|
+
conn.autocommit = False
|
|
49
|
+
return PgConnectionWrapper(conn, _pg_pool)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PgConnectionWrapper:
|
|
53
|
+
"""Wraps psycopg2 connection to match sqlite3 interface."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, conn, pool):
|
|
56
|
+
self._conn = conn
|
|
57
|
+
self._pool = pool
|
|
58
|
+
self._cursor = None
|
|
59
|
+
|
|
60
|
+
def execute(self, sql, params=None):
|
|
61
|
+
# Convert ? placeholders to %s for postgres
|
|
62
|
+
sql = sql.replace("?", "%s")
|
|
63
|
+
# Convert INTEGER PRIMARY KEY AUTOINCREMENT to SERIAL
|
|
64
|
+
sql = sql.replace("INTEGER PRIMARY KEY AUTOINCREMENT", "SERIAL PRIMARY KEY")
|
|
65
|
+
cursor = self._conn.cursor(cursor_factory=_DictCursor())
|
|
66
|
+
cursor.execute(sql, params or ())
|
|
67
|
+
self._cursor = cursor
|
|
68
|
+
return cursor
|
|
69
|
+
|
|
70
|
+
def executescript(self, sql):
|
|
71
|
+
# Convert SQLite syntax to Postgres
|
|
72
|
+
sql = sql.replace("INTEGER PRIMARY KEY AUTOINCREMENT", "SERIAL PRIMARY KEY")
|
|
73
|
+
sql = sql.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX IF NOT EXISTS")
|
|
74
|
+
cursor = self._conn.cursor()
|
|
75
|
+
cursor.execute(sql)
|
|
76
|
+
self._conn.commit()
|
|
77
|
+
return cursor
|
|
78
|
+
|
|
79
|
+
def commit(self):
|
|
80
|
+
self._conn.commit()
|
|
81
|
+
|
|
82
|
+
def close(self):
|
|
83
|
+
self._pool.putconn(self._conn)
|
|
84
|
+
|
|
85
|
+
def fetchone(self):
|
|
86
|
+
if self._cursor:
|
|
87
|
+
return self._cursor.fetchone()
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def fetchall(self):
|
|
91
|
+
if self._cursor:
|
|
92
|
+
return self._cursor.fetchall()
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _DictCursor():
|
|
97
|
+
import psycopg2.extras
|
|
98
|
+
return psycopg2.extras.RealDictCursor
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def init_db(db_path: str = DEFAULT_DB_PATH):
|
|
102
|
+
"""Create all tables if they don't exist."""
|
|
103
|
+
if _is_postgres():
|
|
104
|
+
_init_pg()
|
|
105
|
+
else:
|
|
106
|
+
_init_sqlite(db_path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _init_sqlite(db_path: str):
|
|
110
|
+
conn = sqlite3.connect(db_path, timeout=10)
|
|
111
|
+
conn.row_factory = sqlite3.Row
|
|
112
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
113
|
+
conn.executescript(_SCHEMA_SQLITE)
|
|
114
|
+
conn.commit()
|
|
115
|
+
conn.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _init_pg():
|
|
119
|
+
import psycopg2
|
|
120
|
+
conn = psycopg2.connect(DATABASE_URL)
|
|
121
|
+
cursor = conn.cursor()
|
|
122
|
+
cursor.execute(_SCHEMA_POSTGRES)
|
|
123
|
+
conn.commit()
|
|
124
|
+
conn.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── Schema ──
|
|
128
|
+
|
|
129
|
+
_SCHEMA_SQLITE = """
|
|
130
|
+
-- API keys
|
|
131
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
132
|
+
key_hash TEXT PRIMARY KEY,
|
|
133
|
+
key_prefix TEXT NOT NULL,
|
|
134
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
135
|
+
name TEXT NOT NULL DEFAULT '',
|
|
136
|
+
tier TEXT NOT NULL DEFAULT 'free',
|
|
137
|
+
created_at REAL NOT NULL,
|
|
138
|
+
last_used REAL NOT NULL DEFAULT 0,
|
|
139
|
+
revoked INTEGER NOT NULL DEFAULT 0
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_keys_tenant ON api_keys(tenant_id);
|
|
143
|
+
|
|
144
|
+
-- Gate: agent policies
|
|
145
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
146
|
+
agent_id TEXT NOT NULL,
|
|
147
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
148
|
+
default_scopes TEXT NOT NULL DEFAULT '["read","browse"]',
|
|
149
|
+
max_spend REAL NOT NULL DEFAULT 0.0,
|
|
150
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
151
|
+
created_at REAL NOT NULL,
|
|
152
|
+
PRIMARY KEY (agent_id, tenant_id)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
-- Gate: sessions
|
|
156
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
157
|
+
session_id TEXT PRIMARY KEY,
|
|
158
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
159
|
+
agent_id TEXT NOT NULL,
|
|
160
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
161
|
+
spend_limit REAL NOT NULL DEFAULT 0.0,
|
|
162
|
+
spent REAL NOT NULL DEFAULT 0.0,
|
|
163
|
+
created_at REAL NOT NULL,
|
|
164
|
+
expires_at REAL NOT NULL DEFAULT 0.0,
|
|
165
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON sessions(tenant_id);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
172
|
+
|
|
173
|
+
-- Vault: encrypted secrets
|
|
174
|
+
CREATE TABLE IF NOT EXISTS secrets (
|
|
175
|
+
name TEXT NOT NULL,
|
|
176
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
177
|
+
encrypted_value BLOB NOT NULL,
|
|
178
|
+
scope_required TEXT NOT NULL DEFAULT 'read',
|
|
179
|
+
created_at REAL NOT NULL,
|
|
180
|
+
last_accessed REAL NOT NULL DEFAULT 0.0,
|
|
181
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
182
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
183
|
+
PRIMARY KEY (name, tenant_id)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
-- Vault: payment authorizations
|
|
187
|
+
CREATE TABLE IF NOT EXISTS payments (
|
|
188
|
+
authorization_id TEXT PRIMARY KEY,
|
|
189
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
190
|
+
session_id TEXT NOT NULL,
|
|
191
|
+
agent_id TEXT NOT NULL,
|
|
192
|
+
amount REAL NOT NULL,
|
|
193
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
194
|
+
description TEXT NOT NULL DEFAULT '',
|
|
195
|
+
remaining_budget REAL NOT NULL DEFAULT 0.0,
|
|
196
|
+
timestamp REAL NOT NULL
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_payments_session ON payments(session_id);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_payments_tenant ON payments(tenant_id);
|
|
201
|
+
|
|
202
|
+
-- Watch: audit log
|
|
203
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
204
|
+
entry_id TEXT PRIMARY KEY,
|
|
205
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
206
|
+
session_id TEXT NOT NULL,
|
|
207
|
+
agent_id TEXT NOT NULL,
|
|
208
|
+
action TEXT NOT NULL,
|
|
209
|
+
tool TEXT NOT NULL DEFAULT '',
|
|
210
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
211
|
+
cost_usd REAL NOT NULL DEFAULT 0.0,
|
|
212
|
+
timestamp REAL NOT NULL,
|
|
213
|
+
flagged INTEGER NOT NULL DEFAULT 0,
|
|
214
|
+
flag_reason TEXT NOT NULL DEFAULT ''
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_log(session_id);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_audit_agent ON audit_log(agent_id);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit_log(tenant_id);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tool ON audit_log(tool);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_audit_flagged ON audit_log(flagged);
|
|
223
|
+
|
|
224
|
+
-- Watch: anomaly rules
|
|
225
|
+
CREATE TABLE IF NOT EXISTS anomaly_rules (
|
|
226
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
227
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
228
|
+
rule_type TEXT NOT NULL,
|
|
229
|
+
threshold REAL NOT NULL,
|
|
230
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
231
|
+
created_at REAL NOT NULL
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
-- Approvals
|
|
235
|
+
CREATE TABLE IF NOT EXISTS approval_requests (
|
|
236
|
+
request_id TEXT PRIMARY KEY,
|
|
237
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
238
|
+
session_id TEXT NOT NULL,
|
|
239
|
+
agent_id TEXT NOT NULL,
|
|
240
|
+
action TEXT NOT NULL,
|
|
241
|
+
tool TEXT NOT NULL DEFAULT '',
|
|
242
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
243
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
244
|
+
amount REAL NOT NULL DEFAULT 0.0,
|
|
245
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
246
|
+
created_at REAL NOT NULL,
|
|
247
|
+
expires_at REAL NOT NULL DEFAULT 0.0,
|
|
248
|
+
decided_at REAL NOT NULL DEFAULT 0.0,
|
|
249
|
+
decided_by TEXT NOT NULL DEFAULT '',
|
|
250
|
+
decision_note TEXT NOT NULL DEFAULT ''
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_status ON approval_requests(status);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_tenant ON approval_requests(tenant_id);
|
|
255
|
+
|
|
256
|
+
-- Webhooks
|
|
257
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
258
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
259
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
260
|
+
url TEXT NOT NULL,
|
|
261
|
+
name TEXT NOT NULL DEFAULT '',
|
|
262
|
+
events TEXT NOT NULL DEFAULT '["all"]',
|
|
263
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
264
|
+
created_at REAL NOT NULL,
|
|
265
|
+
last_fired REAL NOT NULL DEFAULT 0,
|
|
266
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
fail_count INTEGER NOT NULL DEFAULT 0
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_tenant ON webhooks(tenant_id);
|
|
271
|
+
|
|
272
|
+
-- Usage tracking (for billing)
|
|
273
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
274
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
275
|
+
tenant_id TEXT NOT NULL,
|
|
276
|
+
month TEXT NOT NULL,
|
|
277
|
+
action_count INTEGER NOT NULL DEFAULT 0,
|
|
278
|
+
secret_access_count INTEGER NOT NULL DEFAULT 0,
|
|
279
|
+
payment_count INTEGER NOT NULL DEFAULT 0,
|
|
280
|
+
total_spend_usd REAL NOT NULL DEFAULT 0.0,
|
|
281
|
+
UNIQUE(tenant_id, month)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_usage_tenant ON usage(tenant_id);
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
_SCHEMA_POSTGRES = _SCHEMA_SQLITE.replace(
|
|
288
|
+
"INTEGER PRIMARY KEY AUTOINCREMENT", "SERIAL PRIMARY KEY"
|
|
289
|
+
)
|