Install Manifest
A registry-agnostic JSON format that an HTTP source can return so the daemon’s /install command knows how to fetch and stage the package without flag input from the agent.
Status: v1 shipping. Implemented in
loader.ts(manifest detection,install_hintsurfacing, path validation) andinstaller.ts(atomic staging, delivery dispatch). Schema additions in v1.x are backwards-compatible — unknown fields are ignored by the daemon.
Web-hosted String apps may be a single markdown page, while multi-file apps are better represented by an install manifest. Agents should not have to remember flags for either case: a plain HTTP markdown page installs as a link, and a manifest can declare its own delivery mode.
The manifest format lets the publisher encode delivery and files decisions at publish time. Daemon respects them; agent types one command for any app.
Schema
Section titled “Schema”When /install <url> fetches a URL and the response is JSON matching this shape, the daemon treats it as an install manifest:
{ "files": [ // REQUIRED — array, non-empty. { "path": "string.md", // path: relative file path inside the package "url": "https://.../string.md" }, // url: HTTPS source for that file { "path": "submolts.md", // either url OR content (string) is required "url": "https://.../submolts.md" }, { "path": "data.md", "content": "...inline markdown..." } // content lets a manifest carry small files inline ], "delivery": "local" | "link" | "any", // OPTIONAL — install mode hint (see below) "install_hint": "Run `/install ...` to ..." // OPTIONAL — markdown shown only on pre-install browse // ── any other fields are ignored by the daemon ──}files[]
Section titled “files[]”The path: 'string.md' entry is the package root and is REQUIRED. Daemon reads its content (via url or inline content) as the SFMD document. A manifest whose files[] is missing the string.md entry is rejected with a clear error before SFMD parsing — without this guard the raw JSON would propagate to the parser as the package source and surface as a misleading “Cannot determine package type” message.
Other entries are package members. Daemon stores them under packages/{name}/{path} for local installs.
Path values must not contain .., must not be absolute (no leading / or \), and must not contain \0. Validation runs before any file is fetched or written — a single bad path aborts the whole install before the network or filesystem is touched, so a malicious manifest can never leave half-written files behind.
install_hint
Section titled “install_hint”Optional markdown string the daemon appends below the package’s string.md content only when the manifest URL is opened pre-install (i.e. via /open <manifest-url> to browse what the package contains). It does NOT enter the persisted package source — once /install runs, future /open app:<name> reads from the local string.md and the hint never echoes back.
Typical use: a marketplace surfaces “Run /install <url> to install this app” as a one-line nudge for agents browsing the catalog.
delivery
Section titled “delivery”| value | daemon behavior |
|---|---|
"local" | Download every files[] entry to packages/{name}/. |
"link" | Don’t download. Register the manifest URL in config.json. Every /open app:{name} re-fetches → /open always sees the publisher’s latest. Linked packages are treated as remote SFMD: HTTP actions can run, CLI actions cannot. |
"any" | Treated as local by default; agent can override with --link. |
| absent | Treated as local. |
Other fields
Section titled “Other fields”Only files[] and delivery have meaning to the daemon. Registries are free to add metadata (source, source_url, version, published_at, description, etc.) — those fields are for the registry’s own UI, ignored by daemon.
Daemon behavior
Section titled “Daemon behavior”When /install <source> runs:
- Local file or directory → existing single-file install. Manifest support doesn’t apply.
- GitHub install source (
github.com/...,gh:..., orraw.githubusercontent.com/...) → local install by default. GitHub is treated as a package source, not a live web app surface. - Other HTTP(S) URL → daemon fetches with
Accept: application/json, text/markdown, text/plain.- Response body parses as JSON AND has
files[]array → treated as manifest:- Loader extracts
files[].path == 'string.md'content for SFMD parsing. - Installer reads
delivery:delivery: 'link'→ register URL only, no file copy.delivery: 'local' | 'any' | undefined→ download allfiles[]topackages/{name}/.
- Loader extracts
- Response is plain markdown → register the URL directly. Every
/open app:{name}re-fetches the page, so web apps stay current. - Response is JSON but has no
files[]array → not a manifest, fall through to plain-markdown handling (likely fails frontmatter parsing — a confusing error, but rare).
- Response body parses as JSON AND has
Flags as overrides
Section titled “Flags as overrides”Agent flags always win over manifest hints:
| flag | effect |
|---|---|
--link | Force link mode even if manifest says delivery: 'local'. |
--local | Force download/local snapshot even if the source would link by default. |
--app / --tool | Override frontmatter type. Same as before. |
--as <local-name> | Override the local registry key. Lets two apps that share (namespace, name)’s name part install side-by-side under different local handles. Independent of manifest contents. |
This keeps power users in control while letting publishers opt into zero-flag UX.
Atomic staging
Section titled “Atomic staging”Multi-file installs stage every fetched file under
packages/.{name}.tmp/, then atomically rename to packages/{name}/
once all files have been validated and written. If any step fails
(bad manifest path, network drop, malformed file), the staging
directory is wiped and the existing packages/{name}/ (if any) is
untouched. Agents never observe a partially-installed package.
Why this isn’t registry coupling
Section titled “Why this isn’t registry coupling”This spec doesn’t favor any specific registry:
- Daemon code has zero hardcoded domains. (Verified by grep — see review notes.)
- The manifest shape (
{ files, delivery }) uses generic, non-vendor-specific keys. - Any HTTP endpoint emitting this shape gets the same handling — StringHub, a future “othermarket.org”, a publisher’s own GitHub Pages, etc.
- Non-conforming endpoints continue working through the existing primitive flags (
--link,--app).
The relationship is the same as Docker Engine ↔ OCI Image Format: the runtime supports a public spec, not a specific registry.
Example
Section titled “Example”Publisher uploads to a marketplace; marketplace serves at https://example.market/api/install/foo/bar:
GET /api/install/foo/barAccept: application/json
200 OKContent-Type: application/json
{ "files": [ { "path": "string.md", "url": "https://example.market/files/foo/bar/string.md" }, { "path": "submolts.md", "url": "https://example.market/files/foo/bar/submolts.md" } ], "delivery": "local"}Agent:
/install https://example.market/api/install/foo/barDaemon:
- Fetches the URL. Content-Type JSON +
files[]→ manifest mode. - Extracts string.md content via the URL in
files[0](or its inlinecontent). - Frontmatter sets
name: bar. Installer createspackages/bar/. delivery: 'local'→ downloads eachfiles[]entry.- Registers
bar: file:///packages/bar/string.mdinconfig.json.
Result: Installed app:bar. Agent never typed a flag.
If delivery had been "link", step 4 would skip; config.json would register the manifest URL itself; result would be Linked app:bar.
Backwards compatibility
Section titled “Backwards compatibility”- Existing daemon test suite (623 tests) passes with the manifest path added.
- Plain markdown HTTP(S) URLs install as linked web apps by default, so
/open app:<name>always fetches the current page. Use--localto snapshot one intopackages/<name>/. --linkand--app/--toolflags work exactly as before; their semantics are unchanged.- No breaking changes to
config.json,packages/, or any user-visible state.
Reference implementation
Section titled “Reference implementation”packages/string/src/loader.ts— JSON manifest detection inloadHttp,install_hintsurfacing, missing-string.mdearly bail.packages/string/src/installer.ts—readManifestDelivery()helper, atomic stage-and-rename, manifest path validation.packages/string/src/commands/packages.ts—--link,--local,--asflag plumbing.
Open questions
Section titled “Open questions”delivery: 'any'— current implementation defaults this to local. Should it be configurable per-daemon?- Manifest versioning — no
versionfield currently. Add one if the spec evolves?