Skip to main content

Reference CLI for the install-manifest spec — scaffold, validate, lint, and diff install manifests. Supports manifest_version 0.1, 0.2, 0.3, 0.3.1, and 0.4.

Project description

Reference CLI — install-manifest

Status: v0.6.0 (2026-06-18) — adds init (scaffold a new valid v0.4 manifest) on top of the read-only / prompt-only subcommands (validate, show, collect-env, lint, diff). The validator dispatches automatically on the manifest's declared manifest_version and supports 0.1, 0.2, 0.3, 0.3.1, and 0.4 (all schemas bundled with the wheel for offline validation). Side-effecting subcommands (install, smoke, revoke) remain pseudocode + architecture below; they will land in subsequent versions, behind their own subcommands and gated by explicit flags.

v0.6 adds init: scaffold a new, schema-valid v0.4 manifest (an mcp-stdio + pip starter) pre-filled with TODO: placeholders, so authoring a manifest no longer starts with reading the schema. install-manifest init --id my-tool writes my-tool.json; -o - streams to stdout; run it bare to fill fields at interactive prompts, or --yes to take defaults for scripts/CI. The generated file passes validate immediately; lint then points at what to harden next.

v0.5 adds two new pre-publish workflows on top of validate: lint runs best-practice rules (kebab-case IDs, SemVer versions, https-only URLs, secret-input constraints, missing verify/kill_switch) and diff classifies changes between two same-version manifests into breaking / additive / cosmetic so publishers can hold themselves to upgrade-safety contracts.

v0.4 adds two manifest surfaces the validator now accepts: runtime.install.method: "preinstalled" (with a required locator of kind python-module / binary-on-path / mcp-server-id) for tools pre-baked into an agent's runtime image, and data_boundary.transmits[].to_kind: "agent-supplied" plus optional to_constraint for outbound destinations supplied by the calling agent at runtime. All v0.3.1 manifests validate unmodified against v0.4 — drop-in upgrade.

This document is the design plan for the full CLI. The shipped slice is described in § Implementation status. Other implementations are welcome and encouraged — the schema is the spec; this is just one client.

Implementation status

Subcommand Shipped Notes
init scaffold a new valid v0.4 manifest (mcp-stdio + pip starter). --id, -o, --force, --yes.
validate fetch + JSON Schema validation against v0.1 / v0.2 / v0.3 / v0.3.1 / v0.4, exit 0/2/3.
show fetch + validate + render consent screen. Read-only.
collect-env fetch + validate + render consent + prompt for env values. No install.
lint fetch + validate + run best-practice rules. --strict, --ignore, --json.
diff fetch + validate two manifests + classify changes. --upgrade-safe, --format.
install Acquires artifacts, runs smoke, persists install record. Deferred.
verify Re-runs smoke for an existing install. Deferred.
revoke Invokes kill_switch. Deferred.
list/status Inspection over the state directory. Deferred.

Install + run

pip install install-manifest
install-manifest init        --id my-tool                                  # scaffold my-tool.json
install-manifest validate    https://toolspace.yepgent.com/examples/gmail.v0.3.json
install-manifest show        https://toolspace.yepgent.com/examples/gmail.v0.3.json
install-manifest collect-env https://toolspace.yepgent.com/examples/gmail.v0.3.json --yes --non-interactive --env GOOGLE_REFRESH_TOKEN=test
install-manifest lint        https://toolspace.yepgent.com/examples/gmail.v0.3.json --strict
install-manifest diff        ./old.json ./new.json --upgrade-safe

Scaffold

install-manifest init writes a new, schema-valid v0.4 manifest so authoring doesn't start from a blank file. The scaffold is an mcp-stdio tool installed via pip — the most common shape — with every field either filled from your answers or set to a schema-valid TODO: placeholder you edit in place.

install-manifest init --id my-tool                 # writes ./my-tool.json
install-manifest init --id my-tool -o -             # stream to stdout
install-manifest init --id my-tool --name "My Tool" --summary "..." --package my-tool-pkg --yes
install-manifest init                               # bare: prompts for id/name/summary/homepage/package/author

Flags: --id (names the output file), --name, --summary, --homepage, --package, --author, -o/--output (default <id>.json; - for stdout), --force (overwrite), -y/--yes (skip prompts — use flags/defaults, for scripts/CI). Run bare in a terminal to fill fields at prompts. Exit 8 if the output exists and --force wasn't given; exit 3 on an invalid id.

The generated manifest passes validate immediately. Then run lint — the starter intentionally trips a couple of advisory rules (e.g. add a verify block, constrain the secret env var) to point you at the next hardening steps. Targets v0.4 only for now; edit runtime afterward for other runtime kinds.

Lint

install-manifest lint <url-or-path> runs schema validation first, then a catalogue of best-practice rules. Severity is warning in v1; default exit is 0, --strict makes any remaining finding return exit 6. Findings are emitted on stderr in the form warning LMxxx /json/pointer: message; --json emits an array on stdout.

Code Rule
LM001 Missing verify block when manifest_version >= 0.3.
LM002 Missing kill_switch when manifest_version >= 0.3.
LM003 data_boundary.transmits[] entry with to_kind=external and no to_constraint.
LM004 data_boundary.transmits[] entries with no to_kind (loose v0.3 — suggest upgrade to v0.4).
LM005 actions[] entry with missing or empty docs.goal.
LM006 verify block present but verify.sla.p95_latency_ms missing.
LM007 tool.id is not kebab-case (^[a-z][a-z0-9]*(-[a-z0-9]+)*$).
LM008 tool.version is not SemVer 2.0.0.
LM009 Any http:// URL anywhere in the manifest (recursive scan; suggest https://).
LM010 env[] entry with secret: true and neither regex/validation_regex nor min_length.

Suppress individual codes with --ignore LM001,LM004.

Diff

install-manifest diff <a> <b> validates both manifests, requires they declare the same manifest_version (v1 hard constraint — raises exit 3 otherwise), and classifies every change into one of three buckets:

  • breaking — a consumer of a cannot transparently upgrade to b. Includes: removed actions / scopes / required env vars / kill_switch; stricter action input schemas (type changes, new required, new enum, additionalProperties true→false); new to_kind=external transmit destinations; same-version-body-changed (version-mutation).
  • additiveb widens capability or guard-rails. New actions, new scope verbs, new optional env vars, newly-added verify or kill_switch blocks.
  • cosmetic — text-only or version-only changes. tool.docs.*, actions[].docs.*, tool.summary edits, tool.version bumps where no other change occurred.

--upgrade-safe makes the command exit 7 if any breaking changes are detected — drop into a CI pipeline to fail publishers who break consumers without bumping a major version. --format=json emits a structured {breaking: [...], additive: [...], cosmetic: [...]} document for tooling.

Cross-version diffing is out of scope for v1. Normalize manually or upgrade the older publisher first.

Local development: cd cli && pip install -e ".[test]" && pytest.

Why this slice first

Side-effecting subcommands (install, smoke, revoke) each have their own risk surface — running shell commands as the user, writing credentials to disk, calling DELETE endpoints with the user's tokens. Shipping them behind the read-only slice gives the spec a chance to harden against manifest authoring mistakes (caught in validate) and consent UX issues (caught in show/collect-env) before any real bytes hit the system.



1. CLI Surface

install-manifest install <manifest_url> [--yes] [--non-interactive] [--state-dir DIR]
install-manifest verify <install_id>
install-manifest list [--state-dir DIR]
install-manifest revoke <install_id> [--yes]
install-manifest status <install_id>
  • install — the primary command. Walks discovery → confirm → env collection → install → smoke → persist.
  • verify — re-runs the smoke test for an existing install.
  • revoke — invokes the manifest's kill_switch and removes local state.
  • list / status — inspection; read-only.
  • --non-interactive — every value must come from env or --env KEY=VAL flags; fails fast on any prompt that would block.
  • --yes — skips the consent confirmation; does not skip env collection.
  • --state-dir — defaults to ~/.local/share/install-manifest/ (XDG-compliant; equivalents on macOS/Windows).

The same binary works for both human-in-loop installs (interactive prompts) and agent-driven installs (--non-interactive --env KEY=VAL).


2. Module Layout

install_manifest/
  __init__.py
  __main__.py          # argparse dispatch
  fetch.py             # fetch_manifest(url) -> dict + raw_bytes
  validate.py          # validate(manifest) -> ValidationResult against schema
  consent.py           # render_consent(manifest) + collect_consent()
  collect_env.py       # collect_env(env_specs, ...) -> dict[str, str]
  install/
    __init__.py        # dispatch on runtime.install.method
    pip.py
    npm.py
    git.py
    container.py
    url.py             # download + sha256 verify
  runtime/
    __init__.py
    mcp_stdio.py       # spawn server, send tool-call, await response
    http.py
    shell.py
  smoke.py             # run_smoke(manifest, install_record)
  state.py             # InstallRecord persistence
  kill.py              # invoke kill_switch
  errors.py            # typed exceptions

Roughly 1500–2500 LOC for a clean v1. Pure stdlib for fetch (urllib); JSON Schema validation via jsonschema (only third-party dependency). Optional keyring for secret storage.


3. The install flow

def cmd_install(manifest_url, *, yes, non_interactive, state_dir, env_overrides) -> int:
    # 1. Fetch
    try:
        manifest_dict, raw_bytes = fetch_manifest(manifest_url, timeout=30)
    except FetchError as e:
        print(f"error: could not fetch manifest at {manifest_url}: {e}", file=sys.stderr)
        return 2

    # 2. Validate against bundled schema
    validation = validate(manifest_dict)
    if not validation.ok:
        print(f"error: manifest invalid: {validation.summary}", file=sys.stderr)
        for path, msg in validation.errors:
            print(f"  {path}: {msg}", file=sys.stderr)
        return 3

    manifest = manifest_dict

    # 3. Consent
    print(render_consent(manifest))  # tool identity, scopes, cost, kill_switch summary
    if not yes:
        if non_interactive:
            print("error: --non-interactive requires --yes", file=sys.stderr)
            return 4
        if not collect_consent():
            print("install cancelled.")
            return 0

    # 4. Collect env
    try:
        env_values = collect_env(
            manifest.get("env", []),
            non_interactive=non_interactive,
            env_overrides=env_overrides,
        )
    except EnvCollectionError as e:
        print(f"error: env collection failed: {e}", file=sys.stderr)
        return 5

    # 5. Install (acquire artifacts)
    install_id = generate_install_id(manifest["tool"]["id"], manifest["tool"]["version"], raw_bytes)
    install_dir = state_dir / "installs" / install_id
    install_dir.mkdir(parents=True, exist_ok=True)

    try:
        install_result = do_install(manifest["runtime"]["install"], install_dir=install_dir)
    except InstallError as e:
        print(f"error: install failed: {e}", file=sys.stderr)
        cleanup(install_dir)  # best-effort
        return 6

    # 6. Persist record (BEFORE smoke, so failures are recoverable)
    record = InstallRecord(
        id=install_id,
        manifest_url=manifest_url,
        manifest_sha256=hashlib.sha256(raw_bytes).hexdigest(),
        manifest=manifest,
        env_values_path=write_env_file(install_dir, env_values),
        install_dir=install_dir,
        installed_at=now(),
        smoke_status="pending",
    )
    save_record(state_dir, record)

    # 7. Smoke
    try:
        smoke_result = run_smoke(manifest, record, timeout_default=30)
    except SmokeError as e:
        record.smoke_status = "error"
        record.smoke_error = str(e)
        save_record(state_dir, record)
        print(f"error: smoke test errored: {e}", file=sys.stderr)
        offer_revoke(record)
        return 7

    if not smoke_result.ok:
        record.smoke_status = "failed"
        record.smoke_failure_reason = smoke_result.failure_reason
        save_record(state_dir, record)
        print(f"smoke failed: {smoke_result.failure_reason}", file=sys.stderr)
        offer_revoke(record)
        return 8

    record.smoke_status = "ok"
    save_record(state_dir, record)

    # 8. Done
    print(f"installed {manifest['tool']['name']} v{manifest['tool']['version']} ({install_id})")
    print(f"  smoke: ok")
    print(f"  revoke with: install-manifest revoke {install_id}")
    return 0

4. Failure-mode handling

Step Failure Action
Fetch network timeout, 404, TLS error exit 2; nothing persisted
Fetch content-type not application/json exit 2; warn the URL may not be a manifest
Validate schema violation exit 3; print every JSON Pointer + error message
Validate manifest_version unsupported exit 3; user's CLI may be wrong version
Consent --non-interactive without --yes exit 4
Consent user declines clean exit 0
Env regex mismatch (interactive) reprompt up to 3 times; on 4th, exit 5
Env regex mismatch (non-interactive) exit 5 immediately
Env required var missing in non-interactive exit 5
Install pip install error, git clone fail, sha256 mismatch exit 6; cleanup install_dir best-effort
Smoke tool process won't start record saved with smoke_status=error; offer revoke
Smoke http timeout / non-2xx record saved with smoke_status=failed; offer revoke
Smoke success criteria not met record saved with smoke_status=failed + which assertion failed; offer revoke
Persist filesystem error exit 9; warn user that install may have happened but is untracked

offer_revoke(record) prompts to invoke kill_switch immediately. Default is "revoke on failed smoke" — leaving credentials live for a tool we couldn't verify is the wrong default.


5. Env collection details

Source resolution order: --env override → existing env var → default → prompt.

Secrets get written to the host keychain (macOS Keychain / Linux Secret Service / Windows Credential Manager via keyring, optional dependency). If keyring unavailable, fall back to ~/.local/share/install-manifest/installs/<id>/.env mode 0600 with a loud warning. Non-secret env vars always go to the .env file.


6. Smoke runner

For each smoke kind:

  • shellsubprocess_run(command, env=load_env(record), timeout=...), evaluate success against exit code + stdout regex.
  • http — request with ${VAR_NAME} expansion in URL/headers/body, evaluate against status + body regex + json_pointer.
  • mcp-tool-call — spawn the MCP server using runtime.entrypoint, send a tool-call request via stdio, evaluate against the result.

Each check_*_success evaluates present fields in success as a logical AND. Returns SmokeResult(ok=True) or SmokeResult(ok=False, failure_reason=...) with the specific assertion that failed.

expand_env_refs does ${VAR_NAME} substitution sourced from the install record's env. This is the only way an HTTP smoke can reference the user's API key without it being baked into the manifest.


7. State directory layout

~/.local/share/install-manifest/
  schema/
    install-manifest-v0.1.json     # bundled copy, read-only
    install-manifest-v0.2.json     # bundled copy, read-only
    install-manifest-v0.3.json     # bundled copy, read-only
    install-manifest-v0.3.1.json   # bundled copy, read-only
    install-manifest-v0.4.json     # bundled copy, read-only
  installs/
    <install_id>/
      manifest.json                 # snapshot of fetched manifest
      manifest.sha256               # checksum of fetched bytes
      record.json                   # InstallRecord
      .env                          # non-secret env values, mode 0600
      artifacts/                    # whatever do_install dropped here
  index.json                        # map install_id -> {tool_id, version, installed_at, smoke_status}

install_id is <tool.id>-<tool.version>-<short-sha-of-manifest-bytes>. Reinstalling the same manifest at the same version is a no-op (or idempotent re-verify). Reinstalling a new version of the same tool gets a new directory; the old install isn't touched until revoke.


8. Deferred to a future CLI release

  • Side-effecting subcommands. install, verify, revoke, list, status are designed (§§ 3-7) but not yet shipped. The current wheel is read-only / prompt-only.
  • Manifest signing. Fetch step does not currently verify a signature. The seller-trust model today is "the user trusted the URL they typed in." Sigstore-style signing is planned.
  • Manifest registry resolution. Today the CLI takes a URL. A future release will accept a registry-relative ID like gmail-yep@1.0.0.
  • Upgrade path. Once install + revoke ship, upgrade will initially be "revoke old, install new"; a later release will add upgrade <install_id> that diffs manifests.
  • Concurrent install protection. Single-invocation-per-state-dir assumption today; lock file planned alongside install.
  • Health scheduler. Smoke runs once at install time (when install ships); optional scheduled re-verification is planned.

9. Open implementation questions

  1. Bundled vs network-fetch schema. Bundle for offline validation, or always fetch from a canonical URL? Current lean: bundle. Network is already used for the manifest itself; bundling avoids a second failure mode.
  2. Keychain dependency. Bail if keyring unavailable, or fall back to plaintext .env with warning? Current lean: warn-and-fallback. Many environments don't have a keychain (Docker, CI, headless servers).
  3. Smoke-on-install vs smoke-deferred. Offer --skip-smoke? Current lean: no, in v0.1. The whole point of smoke is to gate "install succeeded."
  4. Telemetry. Report install/uninstall events to a registry API for inventory tracking? Current lean: opt-in only via --report-to <url>, off by default.
  5. Manifest URL pinning. When saving the install record, record only the URL or also the contents hash? Current lean: both. The hash is the truth-anchor for "was the manifest the same when I installed as it is now?"

PRs welcome on any of these.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

install_manifest-0.6.0.tar.gz (86.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

install_manifest-0.6.0-py3-none-any.whl (74.5 kB view details)

Uploaded Python 3

File details

Details for the file install_manifest-0.6.0.tar.gz.

File metadata

  • Download URL: install_manifest-0.6.0.tar.gz
  • Upload date:
  • Size: 86.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for install_manifest-0.6.0.tar.gz
Algorithm Hash digest
SHA256 96b6de49ab16544414e5679df46e80ad3b6ca3c4d4cbe804c9f61a188dbaa6b2
MD5 8f85e09627d8f754da4cb209adbfc5e7
BLAKE2b-256 045e970cd9e76957487f5c20454698c709f22d7b9ade0a2590dcc728a93c66a7

See more details on using hashes here.

Provenance

The following attestation bundles were made for install_manifest-0.6.0.tar.gz:

Publisher: publish.yml on drknowhow/install-manifest-spec

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file install_manifest-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for install_manifest-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 49cf2c8b872c554045511a0366a8de9795e1ad03f862cc93804d5fb26bb10b90
MD5 3fb81e1698e6d5d858e3283338220ce0
BLAKE2b-256 8f95ed56f98c450002689ce0d6f038e776af75c7dcd994643f614b50584037c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for install_manifest-0.6.0-py3-none-any.whl:

Publisher: publish.yml on drknowhow/install-manifest-spec

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page