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,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,4 @@
1
+ interface:
2
+ display_name: "Toolframe Plugin Design"
3
+ short_description: "Design and build custom Toolframe business plugins"
4
+ default_prompt: "Use $toolframe-plugin-design to design and implement a Toolframe plugin for my business workflow."
@@ -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,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,5 @@
1
+ CREATE TABLE notes (
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ body TEXT NOT NULL CHECK (length(trim(body)) BETWEEN 1 AND 1000),
4
+ created_at TEXT NOT NULL
5
+ );
@@ -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.