Skip to content

Open the datumctl plugin marketplace to third-party catalogs - #224

Merged
scotwells merged 15 commits into
mainfrom
feat/plugin-catalogs
Jul 1, 2026
Merged

scotwells merged 15 commits into
mainfrom
feat/plugin-catalogs

Conversation

@scotwells

@scotwells scotwells commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Pairs with the plugin marketplace enhancement: that PR carries the product vision and design rationale, this one is what it feels like in the hand. Best experienced hands-on — pull the branch, register a catalog, and try search / browse / install.

What this does

Today datumctl can install plugins from a single, Datum-operated catalog. This opens that up: anyone — a community author, an open-source project, or an enterprise platform team — can publish their own catalog, and users can register, search, browse, and install across all of them through the same experience they already know.

You can drive the whole thing from the CLI:

# Add a third-party or internal catalog (one-time trust decision)
datumctl plugin index add acme https://plugins.acme.example/index.yaml

# See where plugins can come from
datumctl plugin index list

# Search and install across every registered catalog
datumctl plugin search dns
datumctl plugin install dns

# Or just browse interactively
datumctl plugin browse

The curated datum catalog stays the trusted, zero-configuration default — nothing changes for users who never add a catalog.

datumctl-third-party-marketplaces.mov

Why it matters

A plugin ecosystem only takes off when people other than Datum can contribute to it. This gives community authors a real front door, gives enterprises a home for internal tooling, and keeps the curated default trustworthy — so the marketplace can grow without every plugin having to flow through Datum's repo.

How it feels

  • One command to add a catalog, with an explicit one-time trust decision so you always know what you're opting into.
  • Search and an interactive browser span every registered catalog at once.
  • Clear official vs. third-party signals wherever a plugin appears — provenance is never ambiguous.
  • Pin the version you want — install a specific plugin release from any catalog.
  • Enterprises are first-class: private internal catalogs, a validate command so authors can check a manifest before publishing, and allow-list guardrails that platform teams can scope down to specific publishers (including individual GitHub owners or repositories) and that keep applying as policy changes — not just at the moment a catalog is added.
  • Safe by default: third-party plugins run with your Datum credentials, so provenance, the one-time trust decision, and credential protection got a careful pass — the curated datum catalog stays the trusted, zero-setup default.

Try it

  1. datumctl plugin index add acme <your-catalog> — see the one-time trust decision.
  2. datumctl plugin search and datumctl plugin browse — watch results span the datum catalog and yours together, each clearly badged.
  3. datumctl plugin install <name> — install across catalogs, pin a version, and see provenance in datumctl plugin list.

Scope & status

The experience — command surface, trust prompts, and the search / browse / install flow — is ready for a hands-on look. The manifest format, catalog APIs, and the path to a real release are still open for discussion alongside the enhancement. One enterprise allow-list refinement (scoping GitHub publishers by owner/repo) changes how an existing allow-list is written; that will be covered in the release notes and docs when it ships.

scotwells and others added 9 commits June 25, 2026 20:41
Add the named-catalog registry, per-catalog index cache, optional
catalog-level manifest header, and managed (enterprise) config that
underpin third-party plugin catalogs. The default catalog stays the
reserved, official, zero-config catalog; existing single-catalog
behavior and old headerless manifests are preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
Add add/list/remove/update/validate subcommands for plugin catalogs,
a one-time third-party trust prompt (with --yes bypass), enterprise
allow-list enforcement on add, managed-catalog protection on remove,
and a manifest linter for publishers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
Install now resolves a bare plugin name across every registered
catalog (default first) and asks the user to qualify the name on a
collision instead of guessing; catalog/name installs from a named
catalog while owner/repo[@Version] stays a direct GitHub install.
Search spans all catalogs with a per-plugin catalog column and trust
badge and a --index scope, and the installed list shows each plugin's
originating catalog and trust badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
Add a type-to-filter browser over every registered catalog that shows
each plugin's catalog and trust badge, lets the user inspect a plugin's
details, and installs the selected plugin in place. Reuses the huh
picker patterns and falls back to a clear message when no terminal is
available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
…ting

A bare-name allow-list entry no longer authorizes a remote catalog
source: because the local catalog name is user-chosen, allowing it to
green-light any host let a trusted name be pointed at an attacker host.
Name entries now authorize local sources only; remote sources must
match a host pattern. Managed pre-seeded catalogs also skip reserved
and malformed names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
Catalog and index-installed plugins are now stored and dispatched under
their plain name (e.g. ipam), recognized via the install record rather
than a datumctl- filename prefix, so milo-os services install cleanly
as datumctl plugins. The datumctl- prefix is retained only for manually
PATH-installed binaries, and a generic name is trusted as a plugin only
from the integrity-checked managed directory — never from a bare PATH
binary. Legacy datumctl-<name> installs in the managed dir still work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
…naries

Add the milo-<command> plugin naming convention so portable milo-os
platform plugins (e.g. milo-ipam) are discovered on PATH alongside
datumctl-<command> plugins; a bare generic name on PATH is still never
treated as a plugin. Archive extraction now prefers the prefixed plugin
binary and only falls back to the bare name last, so an archive that
bundles a service binary sharing the bare name installs the plugin, not
the service. Trust resolution and the untrusted-plugin messages handle
both prefixes. Users still run the plugin as 'datumctl <command>'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
…st counts

Rename the reserved official catalog from "default" to the product-facing
"datum" everywhere it is displayed and addressed, while keeping "default"
as a hidden alias so existing config, install records, and default/<plugin>
addressing keep working. Reserve datum, default, and official so third
parties cannot register them.

'plugin index list' now does a best-effort, concurrent, short-timeout
refresh of any uncached catalog so plugin counts populate consistently
(the official catalog shows its count without a prior search); a catalog
that is genuinely unreachable shows a clear "—" marker and the listing
still succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
Update the OfficialCatalog doc comment to match its name after the
default->datum rename, and switch the DATUMCTL_TRUSTED_PLUGINS parser to
strings.SplitSeq. No functional change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwL2R5VupNJo3C35iLjZnS
@scotwells

scotwells commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Paired with the product vision and design rationale in datum-cloud/enhancements#783 — that proposal explains the why; this PR is a working spike of how it feels in the hand.

scotwells and others added 5 commits June 30, 2026 17:11
Collapse the duplicate trust check in root.go onto the single shared
pluginstore.IsTrusted implementation (DATUMCTL_TRUSTED_PLUGINS env var or
a plugins.json trusted entry whose path + SHA256 match), removing a second
divergent copy that could silently lose the empty-SHA256 fail-closed guard.

Make VerifyManagedPluginIntegrity fail closed: a generic-named managed
binary with no install record, an empty recorded SHA256, or an unreadable
manifest is now rejected instead of being executed. Legacy datumctl-<name>
installs that predate records still run, and any binary with a recorded
hash is still verified. The integrity gate is now also applied on the
completion path, which previously exec'd managed binaries with credentials
on every tab-press with no verification.

Drop t.Parallel() from the two completion tests that mutate global os.Args
so the package is clean under the race detector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third-party catalogs control the host they serve, so manifest and archive
fetches now re-validate every redirect hop: each must stay https (no TLS
downgrade) and may not resolve to a loopback, private, link-local, or
unspecified address (no SSRF / internal port scan). Previously only the
initial URL was checked and http.DefaultClient followed cross-scheme,
cross-host redirects blindly.

Bound every fetch and extraction: manifests and checksums are capped, as
are archive downloads, and each archive entry is size-limited during
decompression. SHA256 only covers the compressed bytes, so without this a
small archive could gunzip/unzip to many gigabytes and exhaust memory. The
manifest cap also bounds the YAML parser against alias-expansion bombs.

Create the catalog cache directory 0700 to match the rest of the store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route an install argument by stripping any @Version suffix first, then
deciding catalog/name vs owner/repo on the remainder, so catalog/name@ver
installs the pinned version from the catalog instead of being misrouted to
GitHub, and a bare name@version resolves across catalogs at that version.
The version is now threaded through to the install instead of dropped.

A third-party 'index add' in a non-interactive shell without --yes now
fails loudly instead of silently succeeding without adding the catalog, so
a CI script that forgot --yes is not misled; an explicit "no" still aborts
cleanly. The trust prompt's wording matches the actual hard block.

Wrap command help in the shared templates helpers, align the search and
list columns, keep empty-result listings on stdout, render the collision
hint as aligned columns, and drop internal artifacts (plugins.json,
--plugin-manifest, api_version) from user-facing help text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Installing a plugin runs the freshly downloaded binary once with
--plugin-manifest to read its metadata. That probe now executes with a
minimal, built-from-scratch environment (PATH, HOME, and temp-dir hints,
plus the Windows equivalents) instead of inheriting the user's full
environment, so Datum credentials, helper hooks, and cloud tokens are not
exposed to a third-party binary during install. Using an allow-list rather
than a deny-list means newly added sensitive variables stay excluded by
default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tHub by owner

An organization's plugin catalog allow-list was only checked when a catalog
was first added, so a catalog added before a policy was published kept
working forever. The allow-list is now applied every time the catalog
registry loads: a catalog whose source is no longer permitted is disabled
and excluded from search, browse, install, and resolution, with a clear
warning explaining why. Disabled catalogs are still listed (marked
disabled) and can still be removed, and the official and organization-
managed catalogs are never disabled. With no allow-list configured nothing
changes.

Allow-list entries can now scope GitHub sources by owner or repository
(github.com/<owner>/*, github.com/<owner>, or github.com/<owner>/<repo>)
instead of authorizing every GitHub repository at once. A plain
raw.githubusercontent.com host entry no longer blanket-authorizes GitHub,
so the scoping is meaningful; local-only and non-GitHub host rules are
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@scotwells scotwells changed the title Prototype: open the datumctl plugin marketplace to third-party catalogs Open the datumctl plugin marketplace to third-party catalogs Jun 30, 2026
@scotwells
scotwells marked this pull request as ready for review June 30, 2026 23:14
…nding screen

The no-argument landing now reflects the plugins a user has added: each
installed plugin is listed as a runnable `datumctl <command>` verb badged
with the catalog it came from, and a new "Extend datumctl" entry plus a few
tips point at `datumctl plugin browse`. The plugin list is read locally and
best-effort, so it never slows down or breaks the landing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@scotwells
scotwells deleted the feat/plugin-catalogs branch July 1, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants