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,339 @@
1
+ """Small durable management journal, physically independent of business stores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import time
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
12
+ from toolframe_sdk import PluginError
13
+
14
+
15
+ def encode(value):
16
+ return json.dumps(value, separators=(",", ":"), allow_nan=False)
17
+
18
+
19
+ class OperationQuery(BaseModel):
20
+ model_config = ConfigDict(extra="forbid")
21
+ page: int = Field(default=1, ge=1)
22
+ page_size: int = Field(default=10, ge=1, le=100)
23
+ through: int | None = Field(default=None, ge=0)
24
+ target: str | None = Field(default=None, max_length=256)
25
+ kind: str | None = Field(default=None, max_length=64)
26
+ status: str | None = Field(default=None, max_length=64)
27
+
28
+
29
+ class SQLiteCoreState:
30
+ def __init__(self, home: Path):
31
+ path = home / "data" / "core.sqlite"
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ self.connection = sqlite3.connect(path)
34
+ path.chmod(0o600)
35
+ self.connection.row_factory = sqlite3.Row
36
+ self.connection.execute("PRAGMA journal_mode=WAL")
37
+ self.connection.execute("PRAGMA busy_timeout=5000")
38
+ version = self.connection.execute("PRAGMA user_version").fetchone()[0]
39
+ if version not in (0, 1, 2):
40
+ self.connection.close()
41
+ raise PluginError(
42
+ "core_schema_incompatible", f"Unsupported core schema {version}"
43
+ )
44
+ self.connection.executescript("""
45
+ CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
46
+ CREATE TABLE IF NOT EXISTS candidates(
47
+ id TEXT PRIMARY KEY, plugin_id TEXT NOT NULL, created REAL NOT NULL, data TEXT NOT NULL
48
+ );
49
+ CREATE TABLE IF NOT EXISTS plugins(
50
+ id TEXT PRIMARY KEY, current_candidate TEXT, previous_candidate TEXT,
51
+ last_candidate TEXT, runtime TEXT NOT NULL DEFAULT '{}', stopped INTEGER NOT NULL DEFAULT 0
52
+ );
53
+ CREATE TABLE IF NOT EXISTS operations(
54
+ id TEXT PRIMARY KEY, kind TEXT NOT NULL, target TEXT, status TEXT NOT NULL,
55
+ phase TEXT NOT NULL, created REAL NOT NULL, updated REAL NOT NULL,
56
+ request TEXT NOT NULL, result TEXT, error TEXT, idempotency TEXT UNIQUE
57
+ );
58
+ """)
59
+ # Serialize the migration and recheck after acquiring the write lock.
60
+ # UUIDs, payloads and idempotency keys survive; old records get stable
61
+ # numbers in creation order, with insertion order breaking timestamp ties.
62
+ with self.connection:
63
+ self.connection.execute("BEGIN IMMEDIATE")
64
+ if self.connection.execute("PRAGMA user_version").fetchone()[0] < 2:
65
+ self.connection.execute(
66
+ "ALTER TABLE operations RENAME TO operations_v1"
67
+ )
68
+ self.connection.execute("""
69
+ CREATE TABLE operations(
70
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, target TEXT,
72
+ status TEXT NOT NULL, phase TEXT NOT NULL,
73
+ created REAL NOT NULL, updated REAL NOT NULL,
74
+ request TEXT NOT NULL, result TEXT, error TEXT,
75
+ idempotency TEXT UNIQUE
76
+ )
77
+ """)
78
+ self.connection.execute("""
79
+ INSERT INTO operations(id,kind,target,status,phase,created,updated,request,result,error,idempotency)
80
+ SELECT id,kind,target,status,phase,created,updated,request,result,error,idempotency
81
+ FROM operations_v1 ORDER BY created, rowid
82
+ """)
83
+ self.connection.execute("DROP TABLE operations_v1")
84
+ self.connection.execute("PRAGMA user_version=2")
85
+ for field in ("target", "kind", "status"):
86
+ self.connection.execute(
87
+ f"CREATE INDEX IF NOT EXISTS operations_{field}_sequence ON operations({field}, sequence)"
88
+ )
89
+ self.connection.commit()
90
+
91
+ def close(self):
92
+ self.connection.close()
93
+
94
+ def meta(self, key, default=None):
95
+ row = self.connection.execute(
96
+ "SELECT value FROM metadata WHERE key=?", (key,)
97
+ ).fetchone()
98
+ return json.loads(row[0]) if row else default
99
+
100
+ def set_meta(self, key, value):
101
+ with self.connection:
102
+ self.connection.execute(
103
+ "INSERT OR REPLACE INTO metadata VALUES(?,?)", (key, encode(value))
104
+ )
105
+
106
+ def save_candidate(self, candidate):
107
+ with self.connection:
108
+ self.connection.execute(
109
+ "INSERT OR REPLACE INTO candidates VALUES(?,?,?,?)",
110
+ (
111
+ candidate["id"],
112
+ candidate["plugin_id"],
113
+ time.time(),
114
+ encode(candidate),
115
+ ),
116
+ )
117
+ self.connection.execute(
118
+ "INSERT OR IGNORE INTO plugins(id) VALUES(?)", (candidate["plugin_id"],)
119
+ )
120
+ self.connection.execute(
121
+ "UPDATE plugins SET last_candidate=? WHERE id=?",
122
+ (candidate["id"], candidate["plugin_id"]),
123
+ )
124
+
125
+ def candidate(self, identifier):
126
+ row = self.connection.execute(
127
+ "SELECT data FROM candidates WHERE id=?", (identifier,)
128
+ ).fetchone()
129
+ if not row:
130
+ raise PluginError("candidate_missing", f"Unknown candidate {identifier}")
131
+ return json.loads(row[0])
132
+
133
+ def plugin(self, identifier):
134
+ row = self.connection.execute(
135
+ "SELECT * FROM plugins WHERE id=?", (identifier,)
136
+ ).fetchone()
137
+ if not row:
138
+ return {
139
+ "id": identifier,
140
+ "current_candidate": None,
141
+ "previous_candidate": None,
142
+ "last_candidate": None,
143
+ "runtime": {},
144
+ "stopped": False,
145
+ }
146
+ result = dict(row)
147
+ result["runtime"] = json.loads(result["runtime"])
148
+ result["stopped"] = bool(result["stopped"])
149
+ return result
150
+
151
+ def plugins(self):
152
+ return [
153
+ self.plugin(row[0])
154
+ for row in self.connection.execute("SELECT id FROM plugins ORDER BY id")
155
+ ]
156
+
157
+ def runtime(self, identifier, value):
158
+ with self.connection:
159
+ self.connection.execute(
160
+ "INSERT OR IGNORE INTO plugins(id) VALUES(?)", (identifier,)
161
+ )
162
+ self.connection.execute(
163
+ "UPDATE plugins SET runtime=? WHERE id=?", (encode(value), identifier)
164
+ )
165
+
166
+ def select(self, identifier, candidate_id):
167
+ with self.connection:
168
+ self.connection.execute(
169
+ "INSERT OR IGNORE INTO plugins(id) VALUES(?)", (identifier,)
170
+ )
171
+ self.connection.execute(
172
+ "UPDATE plugins SET previous_candidate=current_candidate, current_candidate=? WHERE id=?",
173
+ (candidate_id, identifier),
174
+ )
175
+
176
+ def stopped(self, identifier, stopped):
177
+ with self.connection:
178
+ self.connection.execute(
179
+ "INSERT OR IGNORE INTO plugins(id) VALUES(?)", (identifier,)
180
+ )
181
+ self.connection.execute(
182
+ "UPDATE plugins SET stopped=? WHERE id=?", (bool(stopped), identifier)
183
+ )
184
+
185
+ def operation(self, identifier):
186
+ row = self.connection.execute(
187
+ "SELECT * FROM operations WHERE id=?", (identifier,)
188
+ ).fetchone()
189
+ if not row:
190
+ raise PluginError("operation_missing", f"Unknown operation {identifier}")
191
+ result = dict(row)
192
+ for field in ("request", "result", "error"):
193
+ result[field] = (
194
+ json.loads(result[field]) if result[field] is not None else None
195
+ )
196
+ return result
197
+
198
+ def idempotent_operation(self, kind, target, request, idempotency=None):
199
+ if idempotency:
200
+ row = self.connection.execute(
201
+ "SELECT id FROM operations WHERE idempotency=?", (idempotency,)
202
+ ).fetchone()
203
+ if row:
204
+ existing = self.operation(row[0])
205
+ if (
206
+ existing["kind"] != kind
207
+ or existing["target"] != target
208
+ or existing["request"] != request
209
+ ):
210
+ raise PluginError(
211
+ "idempotency_conflict", "Key already belongs to another request"
212
+ )
213
+ return existing
214
+ return None
215
+
216
+ def new_operation(self, kind, target, request, idempotency=None):
217
+ existing = self.idempotent_operation(kind, target, request, idempotency)
218
+ if existing is not None:
219
+ return existing, False
220
+ identifier = uuid.uuid4().hex
221
+ now = time.time()
222
+ with self.connection:
223
+ self.connection.execute(
224
+ "INSERT INTO operations(id,kind,target,status,phase,created,updated,request,result,error,idempotency) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
225
+ (
226
+ identifier,
227
+ kind,
228
+ target,
229
+ "queued",
230
+ "queued",
231
+ now,
232
+ now,
233
+ encode(request),
234
+ None,
235
+ None,
236
+ idempotency,
237
+ ),
238
+ )
239
+ return self.operation(identifier), True
240
+
241
+ def update_operation(
242
+ self, identifier, *, status=None, phase=None, result=None, error=None
243
+ ):
244
+ values = {"updated": time.time()}
245
+ for key, value in (
246
+ ("status", status),
247
+ ("phase", phase),
248
+ ("result", result),
249
+ ("error", error),
250
+ ):
251
+ if value is not None:
252
+ values[key] = encode(value) if key in ("result", "error") else value
253
+ assignments = ",".join(f"{key}=?" for key in values)
254
+ with self.connection:
255
+ self.connection.execute(
256
+ f"UPDATE operations SET {assignments} WHERE id=?",
257
+ (*values.values(), identifier),
258
+ )
259
+
260
+ def operations(self, limit=50):
261
+ return [
262
+ self.operation(row[0])
263
+ for row in self.connection.execute(
264
+ "SELECT id FROM operations ORDER BY sequence DESC LIMIT ?", (limit,)
265
+ )
266
+ ]
267
+
268
+ def operation_page(self, params=None):
269
+ try:
270
+ query = OperationQuery.model_validate(params or {})
271
+ except ValidationError as error:
272
+ raise PluginError(
273
+ "invalid_request", "Invalid operation history query"
274
+ ) from error
275
+ latest = self.connection.execute(
276
+ "SELECT COALESCE(MAX(sequence), 0) FROM operations"
277
+ ).fetchone()[0]
278
+ through = min(query.through, latest) if query.through is not None else latest
279
+ clauses, values = ["sequence <= ?"], [through]
280
+ for field in ("target", "kind", "status"):
281
+ value = getattr(query, field)
282
+ if value is not None:
283
+ if field == "target" and value == "":
284
+ clauses.append("target IS NULL")
285
+ else:
286
+ clauses.append(f"{field}=?")
287
+ values.append(value)
288
+ where = " AND ".join(clauses)
289
+ total = self.connection.execute(
290
+ f"SELECT COUNT(*) FROM operations WHERE {where}", values
291
+ ).fetchone()[0]
292
+ pages = max(1, (total + query.page_size - 1) // query.page_size)
293
+ page = min(query.page, pages)
294
+ items = [
295
+ self.operation(row[0])
296
+ for row in self.connection.execute(
297
+ f"SELECT id FROM operations WHERE {where} ORDER BY sequence DESC LIMIT ? OFFSET ?",
298
+ (*values, query.page_size, (page - 1) * query.page_size),
299
+ )
300
+ ]
301
+ filters = {
302
+ field: [
303
+ row[0]
304
+ for row in self.connection.execute(
305
+ f"SELECT DISTINCT {field} FROM operations WHERE {field} IS NOT NULL ORDER BY {field}"
306
+ )
307
+ ]
308
+ for field in ("target", "kind", "status")
309
+ }
310
+ return {
311
+ "items": items,
312
+ "page": page,
313
+ "page_size": query.page_size,
314
+ "pages": pages,
315
+ "total": total,
316
+ "through": through,
317
+ "newer_count": self.connection.execute(
318
+ "SELECT COUNT(*) FROM operations WHERE sequence > ?", (through,)
319
+ ).fetchone()[0],
320
+ "filters": filters,
321
+ }
322
+
323
+ def reconcile_startup(self, host_run):
324
+ with self.connection:
325
+ self.connection.execute(
326
+ "UPDATE operations SET status='interrupted', updated=? WHERE status IN ('queued','running','cancelling')",
327
+ (time.time(),),
328
+ )
329
+ for plugin in self.plugins():
330
+ historical = plugin["runtime"]
331
+ self.runtime(
332
+ plugin["id"],
333
+ {
334
+ "state": "unknown",
335
+ "host_run": host_run,
336
+ "previous_observation": historical,
337
+ },
338
+ )
339
+ self.connection.execute("UPDATE plugins SET stopped=0")