Reference CLI for the install-manifest spec — fetch, validate, and preview 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.4.0 (2026-05-28) — read-only and prompt-only subcommands implemented (validate, show, collect-env). 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.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 in 0.4.0 | Notes |
|---|---|---|
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. |
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 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
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'skill_switchand removes local state.list/status— inspection; read-only.--non-interactive— every value must come from env or--env KEY=VALflags; 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:
shell—subprocess_run(command, env=load_env(record), timeout=...), evaluatesuccessagainst 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 usingruntime.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,statusare 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+revokeship, upgrade will initially be "revoke old, install new"; a later release will addupgrade <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
installships); optional scheduled re-verification is planned.
9. Open implementation questions
- 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.
- Keychain dependency. Bail if
keyringunavailable, or fall back to plaintext.envwith warning? Current lean: warn-and-fallback. Many environments don't have a keychain (Docker, CI, headless servers). - 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." - Telemetry. Report install/uninstall events to a registry API for inventory tracking? Current lean: opt-in only via
--report-to <url>, off by default. - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file install_manifest-0.4.1.tar.gz.
File metadata
- Download URL: install_manifest-0.4.1.tar.gz
- Upload date:
- Size: 65.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aca8feec42bcee472d6e4cd3efa440306785e396c2208e2d6d72757ff71dbe76
|
|
| MD5 |
60fb53cf28b8b429f1a394b20cbc0ac4
|
|
| BLAKE2b-256 |
764fb9806f569ee08ea23c626537710ffdcaf3559c06714ac398b93325e24f8e
|
File details
Details for the file install_manifest-0.4.1-py3-none-any.whl.
File metadata
- Download URL: install_manifest-0.4.1-py3-none-any.whl
- Upload date:
- Size: 59.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5dab57f624b8302dd575f87839aa3673c6a212dedad6f5189df8c59d5e3a7edc
|
|
| MD5 |
cd8ff1f90112c9c2affb9c9138d3618c
|
|
| BLAKE2b-256 |
6c424f3ea1258e355840071890464eb6a2bb23fc69fe782a45eaa48dfb691964
|