toolframe-core 0.6.2__py3-none-any.whl

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.
Files changed (46) hide show
  1. toolframe_core/__init__.py +3 -0
  2. toolframe_core/_state_postgres.py +420 -0
  3. toolframe_core/_state_sqlite.py +339 -0
  4. toolframe_core/access.py +513 -0
  5. toolframe_core/api.py +700 -0
  6. toolframe_core/bundled_skills/toolframe-plugin-design/LICENSE +21 -0
  7. toolframe_core/bundled_skills/toolframe-plugin-design/SKILL.md +59 -0
  8. toolframe_core/bundled_skills/toolframe-plugin-design/agents/openai.yaml +4 -0
  9. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/LICENSE +21 -0
  10. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/pyproject.toml +21 -0
  11. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/__init__.py +1 -0
  12. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/docs/usage.md +13 -0
  13. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/migrations/0001_init.sql +5 -0
  14. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/page.py +25 -0
  15. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/plugin.py +119 -0
  16. toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/service.py +48 -0
  17. toolframe_core/bundled_skills/toolframe-plugin-design/references/design.md +60 -0
  18. toolframe_core/bundled_skills/toolframe-plugin-design/references/operations.md +133 -0
  19. toolframe_core/bundled_skills/toolframe-plugin-design/references/plugin-api.md +186 -0
  20. toolframe_core/cli.py +681 -0
  21. toolframe_core/config.py +267 -0
  22. toolframe_core/diagnostics.py +200 -0
  23. toolframe_core/graph.py +207 -0
  24. toolframe_core/host.py +2610 -0
  25. toolframe_core/maintenance.py +868 -0
  26. toolframe_core/mcp_access.py +259 -0
  27. toolframe_core/network.py +97 -0
  28. toolframe_core/oidc_access.py +187 -0
  29. toolframe_core/packages.py +728 -0
  30. toolframe_core/process.py +212 -0
  31. toolframe_core/redaction.py +120 -0
  32. toolframe_core/scaffold.py +142 -0
  33. toolframe_core/skills.py +210 -0
  34. toolframe_core/state.py +226 -0
  35. toolframe_core/state_admin.py +270 -0
  36. toolframe_core/stores.py +88 -0
  37. toolframe_core/templates.json +6 -0
  38. toolframe_core/templates.py +56 -0
  39. toolframe_core-0.6.2.dist-info/METADATA +465 -0
  40. toolframe_core-0.6.2.dist-info/RECORD +46 -0
  41. toolframe_core-0.6.2.dist-info/WHEEL +4 -0
  42. toolframe_core-0.6.2.dist-info/entry_points.txt +2 -0
  43. toolframe_core-0.6.2.dist-info/licenses/LICENSE +202 -0
  44. toolframe_core-0.6.2.dist-info/licenses/NOTICE +14 -0
  45. toolframe_core-0.6.2.dist-info/licenses/src/toolframe_core/bundled_skills/toolframe-plugin-design/LICENSE +21 -0
  46. toolframe_core-0.6.2.dist-info/licenses/src/toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """Toolframe's provider-independent control host."""
2
+
3
+ __version__ = "0.6.2"
@@ -0,0 +1,420 @@
1
+ """PostgreSQL management journal; a dedicated database and fenced single owner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import json
8
+ import time
9
+ import uuid
10
+
11
+ from pydantic import ValidationError
12
+ from toolframe_sdk import PluginError
13
+
14
+ from ._state_sqlite import OperationQuery, encode
15
+
16
+ SCHEMA_VERSION = 1
17
+ SCHEMA_SQL = """
18
+ CREATE SCHEMA _toolframe_core;
19
+ REVOKE ALL ON SCHEMA public FROM PUBLIC;
20
+ CREATE TABLE _toolframe_core.identity (
21
+ singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK(singleton),
22
+ application_id TEXT NOT NULL,
23
+ schema_version INTEGER NOT NULL,
24
+ owner_run TEXT,
25
+ owner_node TEXT
26
+ );
27
+ CREATE TABLE _toolframe_core.metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
28
+ CREATE TABLE _toolframe_core.candidates(
29
+ id TEXT PRIMARY KEY, plugin_id TEXT NOT NULL, created DOUBLE PRECISION NOT NULL, data TEXT NOT NULL
30
+ );
31
+ CREATE TABLE _toolframe_core.plugins(
32
+ id TEXT PRIMARY KEY, current_candidate TEXT, previous_candidate TEXT,
33
+ last_candidate TEXT, runtime TEXT NOT NULL DEFAULT '{}', stopped BOOLEAN NOT NULL DEFAULT FALSE
34
+ );
35
+ CREATE TABLE _toolframe_core.operations(
36
+ sequence BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
37
+ id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, target TEXT, status TEXT NOT NULL,
38
+ phase TEXT NOT NULL, created DOUBLE PRECISION NOT NULL, updated DOUBLE PRECISION NOT NULL,
39
+ request TEXT NOT NULL, result TEXT, error TEXT, idempotency TEXT UNIQUE
40
+ );
41
+ CREATE INDEX operations_target_sequence ON _toolframe_core.operations(target,sequence);
42
+ CREATE INDEX operations_kind_sequence ON _toolframe_core.operations(kind,sequence);
43
+ CREATE INDEX operations_status_sequence ON _toolframe_core.operations(status,sequence);
44
+ """
45
+
46
+
47
+ def driver():
48
+ try:
49
+ import psycopg
50
+ except ImportError:
51
+ raise PluginError(
52
+ "core_state_driver_missing",
53
+ "Install toolframe-core[postgres] to use PostgreSQL",
54
+ ) from None
55
+ return psycopg
56
+
57
+
58
+ def lock_key(application_id):
59
+ return int.from_bytes(
60
+ hashlib.sha256(application_id.encode()).digest()[:8], signed=True
61
+ )
62
+
63
+
64
+ def check_identity(row, application_id):
65
+ if not row or row["application_id"] != application_id:
66
+ raise PluginError(
67
+ "core_state_identity_mismatch",
68
+ "Core database belongs to a different application",
69
+ )
70
+ if row["schema_version"] != SCHEMA_VERSION:
71
+ raise PluginError(
72
+ "core_schema_incompatible", "Unsupported PostgreSQL Core schema"
73
+ )
74
+
75
+
76
+ class PostgresCoreState:
77
+ def __init__(self, settings, connection):
78
+ self.settings, self.connection = settings, connection
79
+ self.run = uuid.uuid4().hex
80
+ self.failed = False
81
+ self.claimed = False
82
+
83
+ @classmethod
84
+ async def open(cls, settings):
85
+ pg = driver()
86
+ from psycopg.rows import dict_row
87
+
88
+ connection = None
89
+ state = None
90
+ try:
91
+ async with asyncio.timeout(10):
92
+ connection = await pg.AsyncConnection.connect(
93
+ settings.database_url,
94
+ autocommit=True,
95
+ row_factory=dict_row,
96
+ connect_timeout=5,
97
+ )
98
+ state = cls(settings, connection)
99
+ await connection.execute(
100
+ "SET search_path TO _toolframe_core, pg_catalog"
101
+ )
102
+ await connection.execute("SET statement_timeout TO '5s'")
103
+ await connection.execute("SET lock_timeout TO '2s'")
104
+ role = await state._one(
105
+ "SELECT rolsuper, rolcreatedb, rolcreaterole, rolreplication, rolbypassrls FROM pg_roles WHERE rolname=current_user"
106
+ )
107
+ if any(role.values()):
108
+ raise PluginError(
109
+ "core_state_role_invalid",
110
+ "Core runtime requires a restricted database role",
111
+ )
112
+ acquired = await state._one(
113
+ "SELECT pg_try_advisory_lock(%s) AS acquired",
114
+ (lock_key(settings.application_id),),
115
+ )
116
+ if not acquired["acquired"]:
117
+ raise PluginError(
118
+ "host_already_running",
119
+ "Another Core owns this application database",
120
+ )
121
+ async with connection.transaction():
122
+ row = await state._one(
123
+ "SELECT * FROM identity WHERE singleton FOR UPDATE"
124
+ )
125
+ check_identity(row, settings.application_id)
126
+ if row["owner_run"]:
127
+ raise PluginError(
128
+ "core_state_recovery_required",
129
+ "Previous Core did not release ownership; verify its workers stopped before explicit recovery",
130
+ {
131
+ "owner_run": row["owner_run"],
132
+ "owner_node": row["owner_node"],
133
+ },
134
+ )
135
+ await connection.execute(
136
+ "UPDATE identity SET owner_run=%s, owner_node=%s WHERE singleton",
137
+ (state.run, settings.node_id),
138
+ )
139
+ state.claimed = True
140
+ return state
141
+ except BaseException as error:
142
+ if connection:
143
+ await connection.close()
144
+ if isinstance(error, (pg.Error, TimeoutError)):
145
+ raise PluginError(
146
+ "core_state_unavailable",
147
+ "Cannot open the provisioned Core database",
148
+ ) from None
149
+ raise
150
+
151
+ async def _one(self, query, params=()):
152
+ return await (await self.connection.execute(query, params)).fetchone()
153
+
154
+ async def _all(self, query, params=()):
155
+ return await (await self.connection.execute(query, params)).fetchall()
156
+
157
+ async def call(self, method, *args, **kwargs):
158
+ pg = driver()
159
+ try:
160
+ async with asyncio.timeout(10):
161
+ async with self.connection.transaction():
162
+ row = await self._one(
163
+ "SELECT owner_run FROM identity WHERE singleton"
164
+ )
165
+ if not row or row["owner_run"] != self.run:
166
+ raise PluginError(
167
+ "core_state_ownership_lost",
168
+ "Core journal ownership was lost",
169
+ )
170
+ return await getattr(self, method)(*args, **kwargs)
171
+ except BaseException as error:
172
+ if isinstance(error, (pg.Error, TimeoutError, asyncio.CancelledError)) or (
173
+ isinstance(error, PluginError)
174
+ and error.code == "core_state_ownership_lost"
175
+ ):
176
+ self.failed = True
177
+ await self.connection.close()
178
+ if isinstance(error, asyncio.CancelledError):
179
+ raise
180
+ raise PluginError(
181
+ "core_state_unavailable",
182
+ "Core journal connection failed; a write may have committed. Recover and inspect the operation before retrying",
183
+ ) from None
184
+ raise
185
+
186
+ async def close(self, *, clean=True):
187
+ try:
188
+ if (
189
+ clean
190
+ and self.claimed
191
+ and not self.failed
192
+ and not self.connection.closed
193
+ ):
194
+ async with asyncio.timeout(10):
195
+ await self.connection.execute(
196
+ "UPDATE identity SET owner_run=NULL, owner_node=NULL WHERE singleton AND owner_run=%s",
197
+ (self.run,),
198
+ )
199
+ finally:
200
+ await self.connection.close()
201
+
202
+ async def ping(self):
203
+ return True
204
+
205
+ async def meta(self, key, default=None):
206
+ row = await self._one("SELECT value FROM metadata WHERE key=%s", (key,))
207
+ return json.loads(row["value"]) if row else default
208
+
209
+ async def set_meta(self, key, value):
210
+ await self.connection.execute(
211
+ "INSERT INTO metadata(key,value) VALUES(%s,%s) ON CONFLICT(key) DO UPDATE SET value=EXCLUDED.value",
212
+ (key, encode(value)),
213
+ )
214
+
215
+ async def save_candidate(self, candidate):
216
+ await self.connection.execute(
217
+ "INSERT INTO candidates(id,plugin_id,created,data) VALUES(%s,%s,%s,%s) ON CONFLICT(id) DO UPDATE SET plugin_id=EXCLUDED.plugin_id,created=EXCLUDED.created,data=EXCLUDED.data",
218
+ (candidate["id"], candidate["plugin_id"], time.time(), encode(candidate)),
219
+ )
220
+ await self._ensure_plugin(candidate["plugin_id"])
221
+ await self.connection.execute(
222
+ "UPDATE plugins SET last_candidate=%s WHERE id=%s",
223
+ (candidate["id"], candidate["plugin_id"]),
224
+ )
225
+
226
+ async def candidate(self, identifier):
227
+ row = await self._one("SELECT data FROM candidates WHERE id=%s", (identifier,))
228
+ if not row:
229
+ raise PluginError("candidate_missing", f"Unknown candidate {identifier}")
230
+ return json.loads(row["data"])
231
+
232
+ async def _ensure_plugin(self, identifier):
233
+ await self.connection.execute(
234
+ "INSERT INTO plugins(id) VALUES(%s) ON CONFLICT DO NOTHING", (identifier,)
235
+ )
236
+
237
+ async def plugin(self, identifier):
238
+ row = await self._one("SELECT * FROM plugins WHERE id=%s", (identifier,))
239
+ if not row:
240
+ return {
241
+ "id": identifier,
242
+ "current_candidate": None,
243
+ "previous_candidate": None,
244
+ "last_candidate": None,
245
+ "runtime": {},
246
+ "stopped": False,
247
+ }
248
+ row["runtime"] = json.loads(row["runtime"])
249
+ return row
250
+
251
+ async def plugins(self):
252
+ return [
253
+ await self.plugin(row["id"])
254
+ for row in await self._all("SELECT id FROM plugins ORDER BY id")
255
+ ]
256
+
257
+ async def runtime(self, identifier, value):
258
+ await self._ensure_plugin(identifier)
259
+ await self.connection.execute(
260
+ "UPDATE plugins SET runtime=%s WHERE id=%s", (encode(value), identifier)
261
+ )
262
+
263
+ async def select(self, identifier, candidate_id):
264
+ await self._ensure_plugin(identifier)
265
+ await self.connection.execute(
266
+ "UPDATE plugins SET previous_candidate=current_candidate,current_candidate=%s WHERE id=%s",
267
+ (candidate_id, identifier),
268
+ )
269
+
270
+ async def stopped(self, identifier, stopped):
271
+ await self._ensure_plugin(identifier)
272
+ await self.connection.execute(
273
+ "UPDATE plugins SET stopped=%s WHERE id=%s", (bool(stopped), identifier)
274
+ )
275
+
276
+ @staticmethod
277
+ def _operation(row):
278
+ for field in ("request", "result", "error"):
279
+ row[field] = json.loads(row[field]) if row[field] is not None else None
280
+ return row
281
+
282
+ async def operation(self, identifier):
283
+ row = await self._one("SELECT * FROM operations WHERE id=%s", (identifier,))
284
+ if not row:
285
+ raise PluginError("operation_missing", f"Unknown operation {identifier}")
286
+ return self._operation(row)
287
+
288
+ async def idempotent_operation(self, kind, target, request, idempotency=None):
289
+ if idempotency:
290
+ row = await self._one(
291
+ "SELECT * FROM operations WHERE idempotency=%s", (idempotency,)
292
+ )
293
+ if row:
294
+ existing = self._operation(row)
295
+ if (
296
+ existing["kind"] != kind
297
+ or existing["target"] != target
298
+ or existing["request"] != request
299
+ ):
300
+ raise PluginError(
301
+ "idempotency_conflict", "Key already belongs to another request"
302
+ )
303
+ return existing
304
+ return None
305
+
306
+ async def new_operation(self, kind, target, request, idempotency=None):
307
+ existing = await self.idempotent_operation(kind, target, request, idempotency)
308
+ if existing is not None:
309
+ return existing, False
310
+ now, identifier = time.time(), uuid.uuid4().hex
311
+ row = await self._one(
312
+ "INSERT INTO operations(id,kind,target,status,phase,created,updated,request,idempotency) VALUES(%s,%s,%s,'queued','queued',%s,%s,%s,%s) RETURNING *",
313
+ (identifier, kind, target, now, now, encode(request), idempotency),
314
+ )
315
+ return self._operation(row), True
316
+
317
+ async def update_operation(
318
+ self, identifier, *, status=None, phase=None, result=None, error=None
319
+ ):
320
+ values = {"updated": time.time()}
321
+ for key, value in (
322
+ ("status", status),
323
+ ("phase", phase),
324
+ ("result", result),
325
+ ("error", error),
326
+ ):
327
+ if value is not None:
328
+ values[key] = encode(value) if key in ("result", "error") else value
329
+ assignments = ",".join(f"{key}=%s" for key in values)
330
+ await self.connection.execute(
331
+ f"UPDATE operations SET {assignments} WHERE id=%s",
332
+ (*values.values(), identifier),
333
+ )
334
+
335
+ async def operations(self, limit=50):
336
+ return [
337
+ self._operation(row)
338
+ for row in await self._all(
339
+ "SELECT * FROM operations ORDER BY sequence DESC LIMIT %s", (limit,)
340
+ )
341
+ ]
342
+
343
+ async def operation_page(self, params=None):
344
+ try:
345
+ query = OperationQuery.model_validate(params or {})
346
+ except ValidationError as error:
347
+ raise PluginError(
348
+ "invalid_request", "Invalid operation history query"
349
+ ) from error
350
+ latest = (
351
+ await self._one(
352
+ "SELECT COALESCE(MAX(sequence),0) AS latest FROM operations"
353
+ )
354
+ )["latest"]
355
+ through = min(query.through, latest) if query.through is not None else latest
356
+ clauses, values = ["sequence <= %s"], [through]
357
+ for field in ("target", "kind", "status"):
358
+ value = getattr(query, field)
359
+ if value is not None:
360
+ if field == "target" and value == "":
361
+ clauses.append("target IS NULL")
362
+ else:
363
+ clauses.append(f"{field}=%s")
364
+ values.append(value)
365
+ where = " AND ".join(clauses)
366
+ total = (
367
+ await self._one(
368
+ f"SELECT COUNT(*) AS total FROM operations WHERE {where}", values
369
+ )
370
+ )["total"]
371
+ pages = max(1, (total + query.page_size - 1) // query.page_size)
372
+ page = min(query.page, pages)
373
+ items = [
374
+ self._operation(row)
375
+ for row in await self._all(
376
+ f"SELECT * FROM operations WHERE {where} ORDER BY sequence DESC LIMIT %s OFFSET %s",
377
+ (*values, query.page_size, (page - 1) * query.page_size),
378
+ )
379
+ ]
380
+ filters = {
381
+ field: [
382
+ row[field]
383
+ for row in await self._all(
384
+ f"SELECT DISTINCT {field} FROM operations WHERE {field} IS NOT NULL ORDER BY {field}"
385
+ )
386
+ ]
387
+ for field in ("target", "kind", "status")
388
+ }
389
+ newer = (
390
+ await self._one(
391
+ "SELECT COUNT(*) AS count FROM operations WHERE sequence > %s",
392
+ (through,),
393
+ )
394
+ )["count"]
395
+ return {
396
+ "items": items,
397
+ "page": page,
398
+ "page_size": query.page_size,
399
+ "pages": pages,
400
+ "total": total,
401
+ "through": through,
402
+ "newer_count": newer,
403
+ "filters": filters,
404
+ }
405
+
406
+ async def reconcile_startup(self, host_run):
407
+ await self.connection.execute(
408
+ "UPDATE operations SET status='interrupted',updated=%s WHERE status IN ('queued','running','cancelling')",
409
+ (time.time(),),
410
+ )
411
+ for plugin in await self.plugins():
412
+ await self.runtime(
413
+ plugin["id"],
414
+ {
415
+ "state": "unknown",
416
+ "host_run": host_run,
417
+ "previous_observation": plugin["runtime"],
418
+ },
419
+ )
420
+ await self.connection.execute("UPDATE plugins SET stopped=FALSE")