multica-ai/multica · error

plugin source %q is not a directory or regular ZIP

Error message

plugin source %q is not a directory or regular ZIP

What it means

From loadPluginSource (cmd_plugin.go:149): the --source argument of `plugin validate`/`plugin install` stat'ed successfully but is neither a directory nor a regular file — i.e. a device, socket, pipe, or symlink-to-such. Only directories (packed on the fly) and regular ZIP files are accepted plugin sources.

Source

Thrown at server/cmd/multica/cmd_plugin.go:149

	PluginKey      string `json:"plugin_key"`
	Version        string `json:"version"`
	ManifestDigest string `json:"manifest_digest"`
	ArchiveDigest  string `json:"archive_digest"`
	ArtifactDigest string `json:"artifact_digest"`
	SizeBytes      int64  `json:"size_bytes"`
	FileCount      int    `json:"file_count"`
}

func loadPluginSource(source string) ([]byte, plugincontract.Artifact, error) {
	info, err := os.Stat(source)
	if err != nil {
		return nil, plugincontract.Artifact{}, err
	}
	if info.IsDir() {
		return plugincontract.PackDirectory(source)
	}
	if !info.Mode().IsRegular() {
		return nil, plugincontract.Artifact{}, fmt.Errorf("plugin source %q is not a directory or regular ZIP", source)
	}
	if info.Size() > plugincontract.MaxArchiveSize {
		return nil, plugincontract.Artifact{}, fmt.Errorf("plugin archive exceeds %d bytes", plugincontract.MaxArchiveSize)
	}
	archive, err := os.ReadFile(source)
	if err != nil {
		return nil, plugincontract.Artifact{}, err
	}
	artifact, err := plugincontract.ValidateArtifact(archive)
	return archive, artifact, err
}

func pluginDigestResult(artifact plugincontract.Artifact) pluginDigestOutput {
	return pluginDigestOutput{
		PluginKey: artifact.Manifest.Metadata.Key, Version: artifact.Manifest.Metadata.Version,
		ManifestDigest: plugincontract.DigestBytes(artifact.CanonicalManifest), ArchiveDigest: artifact.ArchiveDigest,
		ArtifactDigest: artifact.ArtifactDigest, SizeBytes: artifact.SizeBytes, FileCount: len(artifact.Files),
	}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Materialize the plugin as a real directory or a regular .zip file and pass that path.
  2. If using process substitution, write the zip to disk first: `zip -r /tmp/plugin.zip dir && multica plugin validate /tmp/plugin.zip`.
  3. Check what the path actually is: `ls -l <path>` — anything not starting with '-' or 'd' will be rejected.

Example fix

// before
multica plugin validate <(make-plugin-zip)   // FIFO -> not a directory or regular ZIP

// after
make-plugin-zip > /tmp/plugin.zip
multica plugin validate /tmp/plugin.zip
Defensive patterns

Strategy: type-guard

Validate before calling

info, err := os.Stat(source)
if err != nil { return err }
if !info.IsDir() && !info.Mode().IsRegular() {
    return fmt.Errorf("%s is mode %v; need directory or regular file", source, info.Mode())
}

Type guard

func isPluginSourcePath(p string) bool {
    info, err := os.Stat(p)
    return err == nil && (info.IsDir() || info.Mode().IsRegular())
}

Prevention

When it happens

Trigger: Passing /dev/null, a named pipe, or a character device as the plugin source; a symlink whose target is a socket/pipe; on some setups a dangling mount point with a special mode.

Common situations: Shell completion or a typo picks up a special file; using process substitution `<(zip ...)` which creates a FIFO; copying a plugin archive with cp from a pipe.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/efb7d4ce0849c706. Report an issue: GitHub.