Develop a Pack
Author a NevoFlux pack — the pack.toml manifest, components, the capability sandbox, lifecycle, and a complete example.
Protocol:
pack-protocol/0.1· Audience: pack authors (first- and third-party)
A pack is a set of files plus a manifest — you do not write installer code. You write declarations and the files they point at; the daemon handles transactional install/uninstall, rollback, an install receipt, and a hard guarantee that uninstall never deletes the user's data by default.
Mental model
your-pack/
├── pack.toml ← the only declaration source
└── components/ ← the files the manifest points at
├── skills/ → copied (flattened) into ~/.config/nevoflux/skills/
├── canvas-tools/*.toml → copied into ~/.config/nevoflux/canvas-tools/
├── seed/*.md → seeded into the GBrain knowledge base (only if absent)
└── canvas-app/dist/ → inserted as a persistent "My Canvas" dashboard artifactThe platform guarantees path safety (files only land in whitelisted dirs), idempotency (re-installing a version is a no-op), transactional install (any failure rolls everything back), and a clean, receipt-driven uninstall that keeps the user's knowledge-base data unless they explicitly purge it.
Quick start
A minimal one-skill pack:
hello-pack/
├── pack.toml
└── components/
└── skills/
└── hello/
└── SKILL.mdpack.toml:
[pack]
name = "hello-pack"
version = "0.1.0"
protocol = "pack-protocol/0.1"
min_nevoflux = "0.3.0"
[components.skills]
dir = "components/skills"Install it (the daemon must be running):
nevoflux pack validate hello-pack/pack.toml # dry capability check, no writes
nevoflux pack install hello-pack/pack.toml
nevoflux pack list # → hello-pack 0.1.0
nevoflux pack uninstall hello-pack # clean removal…or from the browser: Settings → Packs → Install Pack….
The manifest — pack.toml
pack.toml is the single source of truth. All component paths are relative to the
directory that contains it.
[pack] (required)
[pack]
name = "my-pack" # required. [a-z0-9-]+, unique. Also the receipt key and
# the default GBrain namespace prefix.
version = "0.1.0" # required. semver.
protocol = "pack-protocol/0.1" # required. Must be a protocol version the platform supports.
min_nevoflux = "0.3.0" # required. semver lower bound; checked against the daemon version.
description = "One-line summary" # optional.
license = "MIT" # optional.
authors = ["You <you@example>"] # optional.
namespace = "my" # optional. Overrides the GBrain namespace prefix (default = name).Components overview
| Component | Lands in | Removed on uninstall? |
|---|---|---|
[components.skills] | ~/.config/nevoflux/skills/ | yes (sha-guarded) |
[components.canvas_tools] | ~/.config/nevoflux/canvas-tools/ | yes |
[[components.seed]] | GBrain page (only if absent) | no — kept unless --purge-data |
[components.dashboard] | "My Canvas" (persistent artifact) | yes |
[components.protected] | (declaration only) | n/a — marks pages as user data |
[components.knowledge] | — | not supported yet |
Every component is optional. A pack can ship any subset.
Components in depth
Skills — [components.skills]
[components.skills]
dir = "components/skills"dir points at a directory in your pack. Its contents are flattened one level into
the user's skills directory; the loader scans exactly one level and recognizes two
shapes:
components/skills/
├── my-evaluate/SKILL.md → installs as skills/my-evaluate/SKILL.md
├── my-scan/SKILL.md → installs as skills/my-scan/SKILL.md
└── my-quick.md → installs as skills/my-quick.mdA skill file is Markdown with YAML frontmatter. Two rules matter:
allowed-toolsmust use the daemon's real tool names (e.g.browser_navigate,brain_put_page,web_search). A name that doesn't match a registered tool is silently ignored — a typo costs you a tool with no error.- The
dependenciesfield is cosmetic — it's parsed but the loader does nothing with it. To share rules between skills, use conventions (below), notdependencies.
Conventions (shared rules) via skill_read
To share invariants across several skills, ship them as files under a host skill and read them at runtime:
components/skills/
└── my/ ← a "host" skill named `my`
├── SKILL.md
└── conventions/
├── rules.md
└── scoring.mdIn a skill's body, read a convention with skill_read('my', 'conventions/rules.md').
skill_read(name, path) reads files under the named skill's directory (.. traversal
is blocked). Have each skill skill_read its shared rules as its first step.
Canvas tools — [components.canvas_tools]
[components.canvas_tools]
files = ["components/canvas-tools/pdf-render.toml"]
external_binaries = ["weasyprint"] # optional: probed by `pack status`; never executed by the packEach file is a whitelist TOML tool definition, copied to
~/.config/nevoflux/canvas-tools/<basename> and picked up by the canvas-tools loader
(re-scanned on demand — no restart needed). external_binaries are declared so
pack status can tell the user "weasyprint not found — install it"; the pack engine
never runs them.
Seed pages — [[components.seed]]
Use seed for starter/template pages the user will edit. Each seed is written to the knowledge base only if the page doesn't already exist — re-install never clobbers user edits.
[[components.seed]]
slug = "my/cv"
from = "components/seed/cv.template.md"
[[components.seed]]
slug = "my/profile"
from = "components/seed/profile.template.md"slugmust live inside your namespace — equal the namespace or start with<namespace>/.- Every seed slug MUST be covered by
[components.protected]or the pack is rejected at validation. This is the "scaffolding is user data" guarantee. - Seed pages are kept on uninstall by default; only
--purge-dataremoves them.
Protected — [components.protected]
Declares which knowledge-base pages/prefixes are user data that uninstall must never delete by default (declaration only — no files placed):
[components.protected]
slugs = ["my/cv", "my/profile"]
prefixes = ["my/reports/", "my/companies/"]Everything listed must be inside your namespace. A page matches if its slug equals a
slugs entry or starts with a prefixes entry. Rule of thumb: anything your skills
write that represents the user's own data should be under a protected prefix, and
every seed slug must be covered here.
Namespacing
Your pack may only touch knowledge-base pages under its namespace prefix — the
[pack] namespace if set, else [pack] name. Every seed slug and every protected
slug/prefix must equal the namespace or sit under <namespace>/. This keeps two packs
(and the user's own pages) from colliding.
Dashboard — [components.dashboard]
Ships a prebuilt Canvas micro-app as a persistent "My Canvas" artifact:
[components.dashboard]
artifact_id = "my-pack-dashboard" # must start with the pack name
content_type = "project"
files_from = "components/canvas-app/dist" # a directory of built files (index.html + assets)
entry = "index.html" # the entry file within files_fromThe directory's files are packed into the artifact and inserted as persistent, so it
survives session deletion and appears under My Canvas. Re-install upserts the same
artifact_id (no duplicate rows), which must start with the pack name so packs
can't clobber each other's artifacts.
Not yet supported
[components.knowledge]— shipping a whole prebuilt knowledge base as a removable, read-only source is not available inpack-protocol/0.1. A manifest containing it is rejected withKNOWLEDGE_UNSUPPORTED. Ship starter content as[[components.seed]]pages instead.[components.config]— packs may not write toconfig.toml. A manifest containing it is rejected. Express behavior through skills/conventions instead.
The capability sandbox (validation rules)
Before placing a single file, the platform validates your manifest and reports all
violations at once. Run nevoflux pack validate <manifest> to see them without
installing. Your pack must satisfy:
- Whitelisted destinations only — files land solely in
skills/,canvas-tools/, orpacks/<name>/. - No path traversal — every source path must be relative and stay inside the pack;
absolute paths,
..escapes, and backslash separators are rejected on every OS. - No
[components.config]— config writes are forbidden. - Namespace isolation — seed slugs and protected slugs/prefixes must be inside the pack namespace.
seed⊆protected— every seed slug must be covered by a protected slug/prefix (hard reject otherwise — the "never auto-delete user data" guarantee).- Dashboard id namespacing —
dashboard.artifact_idmust start with the pack name.
Parse-time checks also apply: name matches [a-z0-9-]+; version/min_nevoflux are
valid semver; protocol is supported.
Lifecycle & guarantees
Install runs in phases, appending to a receipt and rolling back on any failure:
resolve → compat → capability → idempotency → place files → seed pages → dashboard artifact → activate → commit receipt.
- Idempotent: re-installing the same version is a no-op (use
--forceto reinstall); seed is only-if-absent; the dashboard upserts by id. - Receipt: written to
~/.config/nevoflux/packs/<name>/receipt.json— records every placed file (absolute path + sha256), the dashboard artifact id, and the seeded slugs.
Uninstall is driven entirely by the receipt:
- Deletes the files the pack placed — but skips any file the user has since edited
(sha256 mismatch) unless you pass
--force. - Removes the dashboard artifact and prunes the pack's own dirs.
- Keeps seeded/user pages by default.
--purge-datadeletes the seeded pages (protected pages still refuse to be auto-removed).
Update refreshes the pack's own files/artifacts and adds any new seed pages (only-if-absent), but never touches existing user data.
Testing your pack
-
Validate (no writes):
nevoflux pack validate my-pack/pack.toml— expect{ "ok": true, "violations": [] }. Any violation string (e.g.SeedNotProtected,PathTraversal,SlugOutsideNamespace,ArtifactIdNotNamespaced,ConfigComponentForbidden) tells you exactly what to fix. -
Round-trip in a sandbox so you don't touch your real config:
export XDG_CONFIG_HOME=/tmp/pk-cfg NEVOFLUX_DATA_DIR=/tmp/pk-data mkdir -p /tmp/pk-cfg/nevoflux && : > /tmp/pk-cfg/nevoflux/config.toml nevoflux --daemon & # start a sandboxed daemon nevoflux pack install my-pack/pack.toml nevoflux pack list ls /tmp/pk-cfg/nevoflux/skills/ # verify placement cat /tmp/pk-cfg/nevoflux/packs/my-pack/receipt.json nevoflux pack uninstall my-pack # verify it leaves no trace nevoflux --stop -
Skill lint — confirm every skill's
allowed-toolsentry matches a real tool name (a mismatch is silently dropped).
A complete example
career-pack/
├── pack.toml
└── components/
├── skills/
│ ├── career/ # host skill: conventions live here
│ │ ├── SKILL.md
│ │ └── conventions/{rules,scoring,writing}.md
│ ├── career-evaluate/SKILL.md
│ └── career-scan/SKILL.md
├── canvas-tools/pdf-render.toml
├── seed/{cv,profile}.template.md
└── canvas-app/dist/index.html[pack]
name = "career-pack"
version = "0.1.0"
protocol = "pack-protocol/0.1"
min_nevoflux = "0.3.0"
description = "A job-hunt command center."
license = "MIT"
namespace = "career" # pages live under career/…
[components.skills]
dir = "components/skills"
[components.canvas_tools]
files = ["components/canvas-tools/pdf-render.toml"]
external_binaries = ["weasyprint"]
[[components.seed]]
slug = "career/cv"
from = "components/seed/cv.template.md"
[[components.seed]]
slug = "career/profile"
from = "components/seed/profile.template.md"
[components.dashboard]
artifact_id = "career-pack-dashboard"
content_type = "project"
files_from = "components/canvas-app/dist"
entry = "index.html"
[components.protected]
slugs = ["career/cv", "career/profile"]
prefixes = ["career/reports/", "career/companies/"]career/cv and career/profile are seeded (only if absent) and protected; the
skills read their shared rules via skill_read('career', 'conventions/…'); the dashboard
lands in My Canvas. Uninstall removes the skills, tool, and dashboard but keeps the
user's career/… pages unless --purge-data is passed.
Gotchas / FAQ
- "My skill installed but a tool doesn't work." An
allowed-toolsentry doesn't match a real tool name — it's silently ignored. Check the exact tool name. - "Install was rejected:
SeedNotProtected." Add every seed slug to[components.protected](a slug or a covering prefix). This is mandatory. - "
KNOWLEDGE_UNSUPPORTED." Remove[components.knowledge]— it's deferred. Use[[components.seed]]for starter content. - "
PathTraversal/OutsideWhitelistDir." A source path is absolute, uses.., or uses backslashes. Keep all component paths relative and inside the pack. - "
ArtifactIdNotNamespaced." Prefixdashboard.artifact_idwith your pack name. - "CLI can't reach the daemon." Start it (
nevoflux --daemon), or ensure the browser/native host launched a daemon withpack.*support. - "Uninstall left my edited file." By design — uninstall skips files whose sha256 no
longer matches the receipt. Use
--forceto remove anyway. - "The dashboard didn't update after
update." It upserts byartifact_id; keep the id stable across versions.