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.
- toolframe_core/__init__.py +3 -0
- toolframe_core/_state_postgres.py +420 -0
- toolframe_core/_state_sqlite.py +339 -0
- toolframe_core/access.py +513 -0
- toolframe_core/api.py +700 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/LICENSE +21 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/SKILL.md +59 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/agents/openai.yaml +4 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/LICENSE +21 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/pyproject.toml +21 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/__init__.py +1 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/docs/usage.md +13 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/migrations/0001_init.sql +5 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/page.py +25 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/plugin.py +119 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/assets/sample-plugin/src/toolframe_sample_notes/service.py +48 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/references/design.md +60 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/references/operations.md +133 -0
- toolframe_core/bundled_skills/toolframe-plugin-design/references/plugin-api.md +186 -0
- toolframe_core/cli.py +681 -0
- toolframe_core/config.py +267 -0
- toolframe_core/diagnostics.py +200 -0
- toolframe_core/graph.py +207 -0
- toolframe_core/host.py +2610 -0
- toolframe_core/maintenance.py +868 -0
- toolframe_core/mcp_access.py +259 -0
- toolframe_core/network.py +97 -0
- toolframe_core/oidc_access.py +187 -0
- toolframe_core/packages.py +728 -0
- toolframe_core/process.py +212 -0
- toolframe_core/redaction.py +120 -0
- toolframe_core/scaffold.py +142 -0
- toolframe_core/skills.py +210 -0
- toolframe_core/state.py +226 -0
- toolframe_core/state_admin.py +270 -0
- toolframe_core/stores.py +88 -0
- toolframe_core/templates.json +6 -0
- toolframe_core/templates.py +56 -0
- toolframe_core-0.6.2.dist-info/METADATA +465 -0
- toolframe_core-0.6.2.dist-info/RECORD +46 -0
- toolframe_core-0.6.2.dist-info/WHEEL +4 -0
- toolframe_core-0.6.2.dist-info/entry_points.txt +2 -0
- toolframe_core-0.6.2.dist-info/licenses/LICENSE +202 -0
- toolframe_core-0.6.2.dist-info/licenses/NOTICE +14 -0
- toolframe_core-0.6.2.dist-info/licenses/src/toolframe_core/bundled_skills/toolframe-plugin-design/LICENSE +21 -0
- 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,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 byfb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: toolframe-plugin-design
|
|
3
|
+
version: "0.3.2"
|
|
4
|
+
description: Load when designing, creating, or extending a Toolframe business plugin, including its domain model, persistence, or dashboard and MCP interfaces. Excludes routine application operation, calling existing tools, standalone Streamlit apps, Codex plugins, and Toolframe runtime/provider internals.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Toolframe plugin design
|
|
8
|
+
|
|
9
|
+
Turn the user's business outcome into an independently packaged Toolframe plugin.
|
|
10
|
+
This skill targets Toolframe Core 0.6.1 and SDK 0.3. The attached references and
|
|
11
|
+
starter travel with the application; a Toolframe source checkout is optional.
|
|
12
|
+
|
|
13
|
+
## Establish the design
|
|
14
|
+
|
|
15
|
+
Identify the application home, existing plugin source (if any), and whether the
|
|
16
|
+
request is design-only or includes implementation. Preserve existing plugin IDs
|
|
17
|
+
and data ownership when extending a plugin.
|
|
18
|
+
|
|
19
|
+
Read [design.md](references/design.md) when establishing or changing business
|
|
20
|
+
behavior. Resolve the operations, invariants, data ownership and required user
|
|
21
|
+
surfaces before choosing tables or widgets. Finish this stage with a concise
|
|
22
|
+
design covering those decisions and any consequential unresolved questions.
|
|
23
|
+
For a design-only request, deliver that design and stop before creating code or
|
|
24
|
+
changing the application. An implementation request already authorizes ordinary
|
|
25
|
+
coding; continue without an extra design-approval ceremony.
|
|
26
|
+
|
|
27
|
+
## Implement the plugin
|
|
28
|
+
|
|
29
|
+
For implementation, read [plugin-api.md](references/plugin-api.md) before defining
|
|
30
|
+
services, storage or resources. Use public `toolframe_sdk` contracts. Business
|
|
31
|
+
state belongs in provider-managed storage; business services own its meaning.
|
|
32
|
+
Dashboard pages and MCP tools call those services.
|
|
33
|
+
Declare owned read/write scopes and each method's audience. Use page permissions
|
|
34
|
+
for navigation and controls; core enforces service permissions. Choose each entrance explicitly. Confidential applications need protected Dashboard
|
|
35
|
+
access with `failure_mode="closed"`, a separate protected MCP policy, and no sensitive
|
|
36
|
+
`public_api` methods or llms documents. Preserve an existing application's explicit
|
|
37
|
+
policy until an authorized change. Method scopes do not provide person/tenant or
|
|
38
|
+
row isolation. [plugin-api.md](references/plugin-api.md) defines these boundaries.
|
|
39
|
+
|
|
40
|
+
For a **new** plugin, read [operations.md](references/operations.md), then run
|
|
41
|
+
`toolframe --home APP plugin new PLUGIN-ID --json`. Customize the generated source
|
|
42
|
+
at `APP/plugins/PLUGIN-ID/`; `--output PATH` selects an exact alternative directory.
|
|
43
|
+
The CLI renders the core-packaged [Notes example](assets/sample-plugin), including
|
|
44
|
+
consistent package, module and service identities. Generation alone leaves the
|
|
45
|
+
application composition unchanged. Adapt its schema and behavior to the design;
|
|
46
|
+
remove unneeded presentation resources, UI extras or storage declarations.
|
|
47
|
+
For an **existing** plugin, edit its source directly; do not regenerate it.
|
|
48
|
+
Generated sources belong to the application and survive binary/skill updates.
|
|
49
|
+
Finish implementation with a buildable wheel, explicit scopes and service schemas and
|
|
50
|
+
checks for the requested business rules.
|
|
51
|
+
|
|
52
|
+
## Validate in the application
|
|
53
|
+
|
|
54
|
+
When installing or testing code, read [operations.md](references/operations.md).
|
|
55
|
+
Distinguish package validation, temporary runtime validation and active health.
|
|
56
|
+
Existing-data migrations require a reviewed migration plan and an explicitly
|
|
57
|
+
authorized apply; a normal reload does not apply them. Report evidence for the
|
|
58
|
+
affected service and each requested presentation surface, retained data after
|
|
59
|
+
reload, and any checks not run. An installed package alone is not completion.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 byfb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toolframe-sample-notes"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Small independently packaged Toolframe notes plugin"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
dependencies = ["toolframe-sdk==0.3.0"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
ui = ["streamlit==1.61.0"]
|
|
16
|
+
|
|
17
|
+
[project.entry-points."toolframe.plugins"]
|
|
18
|
+
sample-notes = "toolframe_sample_notes.plugin:plugin"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/toolframe_sample_notes"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Sample Notes business plugin."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Notes
|
|
2
|
+
|
|
3
|
+
Capture a note of 1–1000 characters from the Notes dashboard. Notes are immutable;
|
|
4
|
+
this example does not edit or delete them. The dashboard shows the newest 20.
|
|
5
|
+
|
|
6
|
+
The read-only MCP tool `sample-notes__list_notes` accepts an optional integer
|
|
7
|
+
`limit` from 1 to 100, default 20. It returns `notes` and `count`, where `count` is
|
|
8
|
+
the number in this response. Each note has an `id`, `body` and UTC `created_at`.
|
|
9
|
+
The MCP tool does not create notes.
|
|
10
|
+
|
|
11
|
+
Notes remain stored when the plugin is disabled or reloaded. If a capture's result
|
|
12
|
+
is uncertain after a lost connection, inspect the latest notes before submitting
|
|
13
|
+
again; this sample does not deduplicate separate capture requests.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from toolframe_sdk import PageContext, PluginError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render(context: PageContext):
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
service = context.services.require("sample-notes.notes", version=1)
|
|
8
|
+
st.title("Notes")
|
|
9
|
+
with st.form("sample-notes-capture"):
|
|
10
|
+
body = st.text_area("New note", max_chars=1000, key="sample-notes-body")
|
|
11
|
+
if st.form_submit_button(
|
|
12
|
+
"Capture", disabled=not context.can("sample-notes.write")
|
|
13
|
+
):
|
|
14
|
+
try:
|
|
15
|
+
service.call("capture", {"body": body})
|
|
16
|
+
except PluginError as error:
|
|
17
|
+
st.error(error.message)
|
|
18
|
+
else:
|
|
19
|
+
st.rerun()
|
|
20
|
+
try:
|
|
21
|
+
result = service.call("list_notes", {"limit": 20})
|
|
22
|
+
except PluginError as error:
|
|
23
|
+
st.error(error.message)
|
|
24
|
+
return
|
|
25
|
+
st.dataframe(result["notes"], hide_index=True, width="stretch")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Definitions import without Streamlit or provider implementation dependencies."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
from toolframe_sdk import (
|
|
6
|
+
AuthorizationSpec,
|
|
7
|
+
MethodSpec,
|
|
8
|
+
Migration,
|
|
9
|
+
PluginDefinition,
|
|
10
|
+
Requirement,
|
|
11
|
+
Resource,
|
|
12
|
+
ScopeSpec,
|
|
13
|
+
ServiceSpec,
|
|
14
|
+
StoreSchema,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
SERVICE = "sample-notes.notes"
|
|
18
|
+
PACKAGE = "toolframe_sample_notes"
|
|
19
|
+
NOTE = {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": {"type": "integer", "minimum": 1},
|
|
23
|
+
"body": {"type": "string", "minLength": 1, "maxLength": 1000},
|
|
24
|
+
"created_at": {"type": "string"},
|
|
25
|
+
},
|
|
26
|
+
"required": ["id", "body", "created_at"],
|
|
27
|
+
"additionalProperties": False,
|
|
28
|
+
}
|
|
29
|
+
LIST_INPUT = {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": False,
|
|
35
|
+
}
|
|
36
|
+
LIST_OUTPUT = {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"notes": {"type": "array", "items": NOTE},
|
|
40
|
+
"count": {"type": "integer", "minimum": 0},
|
|
41
|
+
},
|
|
42
|
+
"required": ["notes", "count"],
|
|
43
|
+
"additionalProperties": False,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
plugin = PluginDefinition(
|
|
47
|
+
id="sample-notes",
|
|
48
|
+
factory="toolframe_sample_notes.service:Notes",
|
|
49
|
+
authorization=AuthorizationSpec(
|
|
50
|
+
scopes={
|
|
51
|
+
"sample-notes.read": ScopeSpec(title="Read notes"),
|
|
52
|
+
"sample-notes.write": ScopeSpec(title="Capture notes"),
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
requires={"storage": Requirement(service="toolframe.storage.sql")},
|
|
56
|
+
services={
|
|
57
|
+
SERVICE: ServiceSpec(
|
|
58
|
+
methods={
|
|
59
|
+
"capture": MethodSpec(
|
|
60
|
+
audience="business",
|
|
61
|
+
required_scopes=("sample-notes.write",),
|
|
62
|
+
description="Capture a new note.",
|
|
63
|
+
input_schema={
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {"body": NOTE["properties"]["body"]},
|
|
66
|
+
"required": ["body"],
|
|
67
|
+
"additionalProperties": False,
|
|
68
|
+
},
|
|
69
|
+
output_schema=NOTE,
|
|
70
|
+
),
|
|
71
|
+
"list_notes": MethodSpec(
|
|
72
|
+
audience="business",
|
|
73
|
+
required_scopes=("sample-notes.read",),
|
|
74
|
+
public_api=True,
|
|
75
|
+
description="Read the newest notes.",
|
|
76
|
+
input_schema=LIST_INPUT,
|
|
77
|
+
output_schema=LIST_OUTPUT,
|
|
78
|
+
read_only=True,
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
store=StoreSchema(
|
|
84
|
+
revision="v1",
|
|
85
|
+
migrations=(
|
|
86
|
+
Migration(
|
|
87
|
+
revision="v1",
|
|
88
|
+
sql=files(PACKAGE).joinpath("migrations/0001_init.sql").read_text(),
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
resources=(
|
|
93
|
+
Resource(
|
|
94
|
+
id="notes",
|
|
95
|
+
kind="dashboard.page",
|
|
96
|
+
title="Notes",
|
|
97
|
+
required_scopes=("sample-notes.read",),
|
|
98
|
+
data={"entrypoint": "toolframe_sample_notes.page:render", "order": 10},
|
|
99
|
+
),
|
|
100
|
+
Resource(
|
|
101
|
+
id="list_notes",
|
|
102
|
+
kind="mcp.tool",
|
|
103
|
+
title="List notes",
|
|
104
|
+
data={
|
|
105
|
+
"service": SERVICE,
|
|
106
|
+
"method": "list_notes",
|
|
107
|
+
"description": "Read the newest captured notes.",
|
|
108
|
+
"input_schema": LIST_INPUT,
|
|
109
|
+
"output_schema": LIST_OUTPUT,
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
Resource(
|
|
113
|
+
id="usage",
|
|
114
|
+
kind="llms.document",
|
|
115
|
+
title="Notes",
|
|
116
|
+
data={"package": PACKAGE, "path": "docs/usage.md"},
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from toolframe_sdk import Plugin, PluginError
|
|
4
|
+
|
|
5
|
+
from .plugin import SERVICE
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Notes(Plugin):
|
|
9
|
+
async def capture(self, body):
|
|
10
|
+
if not isinstance(body, str) or not 1 <= len(body.strip()) <= 1000:
|
|
11
|
+
raise PluginError(
|
|
12
|
+
"note_invalid", "A note needs 1–1000 nonblank characters."
|
|
13
|
+
)
|
|
14
|
+
body = body.strip()
|
|
15
|
+
async with self.context.storage().transaction() as tx:
|
|
16
|
+
created = await tx.execute(
|
|
17
|
+
"INSERT INTO notes(body, created_at) VALUES (?, ?)",
|
|
18
|
+
(body, datetime.now(UTC).isoformat()),
|
|
19
|
+
)
|
|
20
|
+
rows = await tx.query(
|
|
21
|
+
"SELECT id, body, created_at FROM notes WHERE id = ?",
|
|
22
|
+
(created["lastrowid"],),
|
|
23
|
+
)
|
|
24
|
+
return rows[0]
|
|
25
|
+
|
|
26
|
+
async def list_notes(self, limit=20):
|
|
27
|
+
if (
|
|
28
|
+
isinstance(limit, bool)
|
|
29
|
+
or not isinstance(limit, int)
|
|
30
|
+
or not 1 <= limit <= 100
|
|
31
|
+
):
|
|
32
|
+
raise PluginError("note_invalid", "limit must be an integer from 1 to 100.")
|
|
33
|
+
notes = await self.context.storage().query(
|
|
34
|
+
"SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT ?", (limit,)
|
|
35
|
+
)
|
|
36
|
+
return {"notes": notes, "count": len(notes)}
|
|
37
|
+
|
|
38
|
+
async def invoke(self, service, method, params):
|
|
39
|
+
if service == SERVICE:
|
|
40
|
+
if method == "capture":
|
|
41
|
+
return await self.capture(**params)
|
|
42
|
+
if method == "list_notes":
|
|
43
|
+
return await self.list_notes(**params)
|
|
44
|
+
raise PluginError("method_not_found", f"Unknown method {service}/{method}")
|
|
45
|
+
|
|
46
|
+
async def health(self):
|
|
47
|
+
await self.context.storage().query("SELECT id FROM notes LIMIT 1")
|
|
48
|
+
return {"runtime": "ready", "business": "ready"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Business design
|
|
2
|
+
|
|
3
|
+
Use the user's vocabulary to define the smallest useful business capability.
|
|
4
|
+
Toolframe's plugin boundary is a deployable package and a data owner; it need not
|
|
5
|
+
be a folder per screen or a separate plugin for every entity.
|
|
6
|
+
|
|
7
|
+
## From workflow to contract
|
|
8
|
+
|
|
9
|
+
Record these decisions in the plugin's own design notes, at a level proportional
|
|
10
|
+
to the request. Keep the notes outside the files published by `llms.document`.
|
|
11
|
+
|
|
12
|
+
| Decision | Evidence needed before implementation |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Outcome and actors | Who performs which operation, and what successful completion means. |
|
|
15
|
+
| Vocabulary and identity | Stable business identities and the meaning of statuses, units, dates and money. |
|
|
16
|
+
| Invariants | Rules that must hold even when a page, MCP tool and another plugin call concurrently. |
|
|
17
|
+
| Data ownership | Which plugin owns each record; which services expose it to other plugins. |
|
|
18
|
+
| Commands and queries | Inputs, outputs, validation failures, conflict behavior and required read/write permissions. |
|
|
19
|
+
| Presentation | Required pages/tools/docs and the behavior when those optional providers are absent. |
|
|
20
|
+
| Persistence and change | Constraints, indexes, transaction boundaries and whether existing data needs migration. |
|
|
21
|
+
|
|
22
|
+
For example, inventory requires an explicit choice between an editable current
|
|
23
|
+
balance and an append-only movement history. If corrections must be explainable,
|
|
24
|
+
model receipts, issues and reversals, and derive stock from movements. Decide
|
|
25
|
+
whether negative stock is allowed and check it in the same transaction as a new
|
|
26
|
+
movement. A table with an unrestricted editable quantity does not establish that
|
|
27
|
+
rule. Choose according to the actual workflow, not this example by default.
|
|
28
|
+
|
|
29
|
+
Use stable IDs for references, not display names. Define timezone and date-only
|
|
30
|
+
semantics. For money, choose a currency and exact representation such as integer
|
|
31
|
+
minor units; an unrestricted binary float is not an accounting contract. Add
|
|
32
|
+
uniqueness and database constraints for rules that persist independently of UI.
|
|
33
|
+
|
|
34
|
+
## Concurrency and side effects
|
|
35
|
+
|
|
36
|
+
Enforce invariants in the business service. The page can explain a validation
|
|
37
|
+
failure but must not be its only enforcement point. For editable records, use a
|
|
38
|
+
version or another explicit conflict strategy; a stale editor should not silently
|
|
39
|
+
replace a newer write. Keep related same-owner changes in one managed transaction.
|
|
40
|
+
|
|
41
|
+
Other plugins consume a declared, versioned service rather than opening this
|
|
42
|
+
plugin's SQLite file. There is no cross-owner SQL transaction. If a workflow
|
|
43
|
+
changes multiple owners, define its intermediate states and reconciliation.
|
|
44
|
+
|
|
45
|
+
Storage mutation receipts can help resolve an uncertain database write. They do
|
|
46
|
+
not make an entire business method or external HTTP side effect exactly-once.
|
|
47
|
+
Define an application-level idempotency key when safe retries are a requirement;
|
|
48
|
+
otherwise surface an unknown outcome for reconciliation instead of replaying it.
|
|
49
|
+
|
|
50
|
+
## Scope and completion
|
|
51
|
+
|
|
52
|
+
Ask only about choices that change behavior and cannot be inferred from the
|
|
53
|
+
request or existing source. Record reversible defaults and continue independent
|
|
54
|
+
work. Design-only completion is a reviewable model and contract, including any
|
|
55
|
+
open business decisions; it does not require scaffolding or starting a server.
|
|
56
|
+
|
|
57
|
+
During implementation, keep the stable plugin ID and previously applied migration
|
|
58
|
+
SQL bytes. Append migrations for schema changes, including data backfills and
|
|
59
|
+
their failure handling. Complete the design when every requested operation maps
|
|
60
|
+
to an owning service, data changes, user feedback and a checkable outcome.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Build, install and validate
|
|
2
|
+
|
|
3
|
+
Use the application's actual executable and home. The examples use `toolframe`
|
|
4
|
+
on PATH; `--home APP` goes before the subcommand. Consult `--help` for the installed
|
|
5
|
+
version rather than assuming commands from another Toolframe generation.
|
|
6
|
+
|
|
7
|
+
## New source and application
|
|
8
|
+
|
|
9
|
+
For a new plugin, generate the source with the installed CLI:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
toolframe --home APP plugin new customer-notes --json
|
|
13
|
+
# Alternative: the exact destination directory, relative to the current directory.
|
|
14
|
+
toolframe --home APP plugin new customer-notes --output ./custom/customer-notes
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Choose one of these destinations. The default is `APP/plugins/customer-notes/`.
|
|
18
|
+
The CLI sets distribution `toolframe-customer-notes`, module
|
|
19
|
+
`toolframe_customer_notes`, plugin ID `customer-notes` and service namespace
|
|
20
|
+
`customer-notes.*`. JSON returns `plugin_id`, `package_name`, `python_module`,
|
|
21
|
+
`path` and `status: "created"`. An existing file, directory or symlink is refused.
|
|
22
|
+
IDs ending in `-` keep their plugin ID but add `plugin` to the distribution's end
|
|
23
|
+
to form a valid package name; the module uses its underscores form. Read the
|
|
24
|
+
returned names when editing references.
|
|
25
|
+
An existing plugin is edited directly, never regenerated or overwritten.
|
|
26
|
+
|
|
27
|
+
Generation uses the template bundled inside core, without needing this skill to
|
|
28
|
+
be installed or a Toolframe source checkout. It does not build, install, register,
|
|
29
|
+
enable or start a plugin, read application configuration, or request a management
|
|
30
|
+
token. The launcher may prepare its own core runtime on first use as usual.
|
|
31
|
+
Generated plugins are independent source trees; skill/binary updates leave them
|
|
32
|
+
alone. `plugins/` is an authoring location, not an auto-discovery mechanism.
|
|
33
|
+
|
|
34
|
+
Customize the generated files for the agreed design. The Notes example provides
|
|
35
|
+
managed SQLite, capture/list services, a Streamlit page, a read-only MCP list tool
|
|
36
|
+
and llms documentation. A headless service can omit the page, tools, docs, `ui`
|
|
37
|
+
extra and storage when not needed. Keep its package and resource references
|
|
38
|
+
consistent when intentionally changing identity.
|
|
39
|
+
|
|
40
|
+
Build with `uv build --wheel` from the generated plugin directory. The starter contains no
|
|
41
|
+
sibling path dependencies. Where Toolframe packages are not in the configured
|
|
42
|
+
index, use an existing wheelhouse for dependency resolution. For a new local
|
|
43
|
+
application:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
toolframe --home APP init
|
|
47
|
+
toolframe --home APP serve
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run `serve` in a separate terminal. Add required providers to the composition
|
|
51
|
+
before enabling a plugin that consumes their services. For a local SQLite wheel:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
toolframe --home APP plugin add /path/to/toolframe_storage_sqlite-0.3.1-py3-none-any.whl --enable
|
|
55
|
+
toolframe --home APP plugin add APP/plugins/customer-notes
|
|
56
|
+
toolframe --home APP plugin validate customer-notes
|
|
57
|
+
toolframe --home APP plugin validate customer-notes --runtime
|
|
58
|
+
toolframe --home APP plugin enable customer-notes
|
|
59
|
+
toolframe --home APP plugin status customer-notes --check
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use your generated path and ID in place of `customer-notes`. `add` validates and leaves the plugin
|
|
63
|
+
disabled unless `--enable` is explicit. Default validation covers package,
|
|
64
|
+
dependencies, imports, configuration and resources. `--runtime` runs against a
|
|
65
|
+
temporary data environment; it does not establish production health or prove a
|
|
66
|
+
migration of existing data. Use sandbox configuration for plugins with external
|
|
67
|
+
side effects. Active health is checked after enable/reload.
|
|
68
|
+
|
|
69
|
+
`toolframe.toml` is the desired composition. CLI/API changes use the same core
|
|
70
|
+
operations; core SQLite records their actual state. For offline or unpublished
|
|
71
|
+
packages, use the launcher's `--wheelhouse PATH` and the composition's
|
|
72
|
+
`[packages].find_links`. Warm-cache `--offline` does not download missing assets.
|
|
73
|
+
|
|
74
|
+
## Local edits and reload
|
|
75
|
+
|
|
76
|
+
In the commands below, replace `my-plugin` with the definition's ID.
|
|
77
|
+
Editing source does not modify an active immutable snapshot, even if its package
|
|
78
|
+
version is unchanged. Validate and reload explicitly:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
toolframe --home APP plugin validate my-plugin
|
|
82
|
+
toolframe --home APP plan --action reload --id my-plugin
|
|
83
|
+
toolframe --home APP plugin reload my-plugin --plan-id REVIEWED_PLAN_ID
|
|
84
|
+
toolframe --home APP plugin status my-plugin --check
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The operation's `result.plan_id` identifies the reviewed plan. Recompute a stale
|
|
88
|
+
plan instead of replaying it. `plugin update ID --source PATH` stages a different
|
|
89
|
+
source; reload performs the switch. Operations normally wait; `--no-wait` returns
|
|
90
|
+
an operation ID for `operation status ID`.
|
|
91
|
+
|
|
92
|
+
Normal drain timeout aborts replacement and retains the old runtime. If activation
|
|
93
|
+
fails and cleanup/schema conditions permit it, core tries the retained old
|
|
94
|
+
snapshot once; `failed_recovered` still means this reload failed. Inspect candidate
|
|
95
|
+
status, active status and `plugin logs ID`, not only the installed version. When
|
|
96
|
+
the plan reports `restart_required`, follow that diagnosis instead of forcing a
|
|
97
|
+
reload. Dashboard sessions may reconnect; MCP catalog/schema updates require
|
|
98
|
+
client reconnection. Do not retry an uncertain business write automatically.
|
|
99
|
+
|
|
100
|
+
## Existing-data schema changes
|
|
101
|
+
|
|
102
|
+
Append a migration and stage the candidate. Then:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
toolframe --home APP migrate plan my-plugin
|
|
106
|
+
toolframe --home APP migrate apply my-plugin --plan-id REVIEWED_MIGRATION_PLAN_ID
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Review the plan's affected cohort, schema change and backup/recovery implications.
|
|
110
|
+
Apply only within the user's authorization to change that application's data;
|
|
111
|
+
finish the migration code and reviewable plan before requesting any still-missing
|
|
112
|
+
authorization. The explicit apply operation performs the schema change and runtime
|
|
113
|
+
switch. An ordinary reload, including watch mode, cannot substitute for it. A
|
|
114
|
+
committed schema upgrade is not automatically undone when new code fails; preserve
|
|
115
|
+
the database and perform forward repair or an explicitly chosen restore.
|
|
116
|
+
|
|
117
|
+
## Evidence for completion
|
|
118
|
+
|
|
119
|
+
Verify business invariants through public services and requested interfaces.
|
|
120
|
+
For a Dashboard plugin, also verify a read-only password cannot write, including a
|
|
121
|
+
direct protected service call. Check anonymous, reader, writer and local operator
|
|
122
|
+
access separately through Dashboard/service calls, explicit public HTTP methods,
|
|
123
|
+
and an actual MCP client. In a disposable application, disable the Dashboard auth
|
|
124
|
+
provider and verify closed policy denies protected calls, public HTTP remains
|
|
125
|
+
intentional, and protected MCP keeps its independent gate. Restore auth, revoke a
|
|
126
|
+
grant, and verify denial and recovery. Back up configuration/data before testing
|
|
127
|
+
failure or reload in an existing application. Scope tests do not prove row isolation.
|
|
128
|
+
Record preflight, scratch runtime and active health separately. For persistent
|
|
129
|
+
plugins, create synthetic data in a disposable application, reload after a real
|
|
130
|
+
local edit, and show that the behavior updates while old records remain. Check
|
|
131
|
+
absence of optional providers where headless use is required. Test each requested
|
|
132
|
+
page/tool/docs surface and disclose any unavailable checks; do not count a schema
|
|
133
|
+
or package build as a successful user workflow.
|