Repository navigation
Plugin Authoring
Write a custom check, distribute it as a Python package, and have it run alongside the built-in catalog.
Plugins break the zero-execution trust guarantee when enabled. Treat plugin code as arbitrary Python and review accordingly. Plugins are off by default.
A plugin is a Python callable with this signature:
from agents_shipgate.core.context import ScanContext
from agents_shipgate.core.models import Finding
def run(context: ScanContext) -> list[Finding]:
...Optionally attach metadata so the check shows up in agents-shipgate list-checks and explain:
from agents_shipgate.core.models import CheckMetadata
run.AGENTS_SHIPGATE_METADATA = CheckMetadata(
id="ACME-COMPLIANCE-PII-MASKING",
category="compliance",
default_severity="high",
description="Tools that handle PII must declare a masking policy.",
rationale="Regulatory requirement; release reviewers need this evidence.",
fires_when="Tool has sensitive_data_access tag without acme.compliance.pii_masking annotation.",
evidence_fields=["risk_tags", "annotation_present"],
recommendation="Add the acme.compliance.pii_masking annotation or document an exemption.",
)CheckMetadata accepts a dict in place of the model — handy if you don't want to import shipgate at the metadata level.
# acme_shipgate_checks/compliance.py
from agents_shipgate.checks.base import tool_finding
from agents_shipgate.core.context import ScanContext
from agents_shipgate.core.models import CheckMetadata, Finding
from agents_shipgate.core.risk_hints import has_risk_tag
def run(context: ScanContext) -> list[Finding]:
findings: list[Finding] = []
for tool in context.tools:
if not has_risk_tag(tool, {"sensitive_data_access"}, min_confidence="medium"):
continue
if tool.annotations.get("acme.compliance.pii_masking") is True:
continue
findings.append(
tool_finding(
tool=tool,
check_id="ACME-COMPLIANCE-PII-MASKING",
title=f"{tool.name} handles PII without a masking policy annotation",
severity="high",
category="compliance",
evidence={
"risk_tags": [hint.tag for hint in tool.risk_hints],
"annotation_present": False,
},
confidence="high",
recommendation=(
f"Set acme.compliance.pii_masking: true on {tool.name} "
"or add a documented exemption."
),
context=context,
)
)
return findings
run.AGENTS_SHIPGATE_METADATA = CheckMetadata(
id="ACME-COMPLIANCE-PII-MASKING",
category="compliance",
default_severity="high",
description="Tools that handle PII must declare a masking policy.",
rationale="Regulatory requirement; release reviewers need this evidence.",
fires_when="Tool has sensitive_data_access tag without acme.compliance.pii_masking annotation.",
evidence_fields=["risk_tags", "annotation_present"],
recommendation="Add the annotation or document an exemption.",
)tool_finding(...) and agent_finding(...) are exported helpers that handle severity/confidence parsing and source-reference assembly. Importing them is recommended over building Finding objects by hand — they keep your evidence format consistent with the built-ins.
pyproject.toml:
[project]
name = "acme-shipgate-checks"
version = "1.2.3"
requires-python = ">=3.12"
dependencies = ["agents-shipgate>=0.10,<0.11"]
[project.entry-points."agents_shipgate.checks"]
compliance = "acme_shipgate_checks.compliance:run"The entry point group is agents_shipgate.checks. The entry-point name (compliance here) is for human inspection — the actual check ID is whatever your AGENTS_SHIPGATE_METADATA.id (or check_id; the v0.17 alias) declares.
Custom adapters (new
tool_sources[].typesource families) are a separate extension point: register a class satisfying theToolSourceAdapterProtocol under theagents_shipgate.adaptersentry-point group. Same opt-in env var and--no-pluginskill switch as check plugins. Seedocs/framework-adapter-checklist.md.
Install in the same environment as agents-shipgate:
pip install agents-shipgate==0.10.0 acme-shipgate-checks==1.2.3Plugins are off by default. Opt in per-environment:
export AGENTS_SHIPGATE_ENABLE_PLUGINS=1
agents-shipgate scan -c shipgate.yamlOr for a single invocation:
AGENTS_SHIPGATE_ENABLE_PLUGINS=1 agents-shipgate scan -c shipgate.yamlTo force plugins off for a specific scan even when the env var is set:
agents-shipgate scan --no-plugins
agents-shipgate list-checks --no-plugins
agents-shipgate explain --no-plugins SHIP-POLICY-APPROVAL-MISSINGThe action exposes the same:
- uses: ThreeMoonsLab/agents-shipgate@v0.10.0
with:
no_plugins: 'true'When a plugin runs, the JSON report's loaded_plugins array enumerates every contributing entry point:
"loaded_plugins": [
{
"name": "compliance",
"value": "acme_shipgate_checks.compliance:run",
"distribution": "acme-shipgate-checks",
"version": "1.2.3",
"check_id": "ACME-COMPLIANCE-PII-MASKING"
}
]The Markdown report includes a "Loaded Plugins" section with the same data. Release reviewers can see exactly which third-party packages contributed findings.
Every entry point is checked against five load-time gates before it is allowed to
run. The result is recorded in loaded_plugins[].validation_status (one of
valid | load_failed | bad_signature | bad_metadata | dynamic_default_not_supported | id_collision | bad_floor) — invalid plugins still appear in the array (with check_id: null when they failed before metadata parsing) so reviewers can see what was skipped without reading scanner logs:
-
load —
entry_point.load()must not raise. -
signature — the loaded object must be callable and accept exactly one required positional parameter (
ScanContext); extra defaulted/keyword-only params are fine. -
metadata —
AGENTS_SHIPGATE_METADATAmust be present and parse asCheckMetadata. Bothidandcheck_idare accepted as the identifier key (v0.17 alias); prefercheck_idfor symmetry withFinding.check_id. - id_collision — your check ID must not shadow a built-in (including legacy aliases) or a previously-registered plugin.
-
bad_floor —
floor_severitymust not exceeddefault_severityon the same metadata block.
At runtime, Shipgate additionally drops any finding whose Finding.check_id
does not match your declared id/check_id (the check_id-smuggling guard),
drops non-Finding items, and captures any exception raised during your plugin
call into loaded_plugins[].runtime_errors. The scan continues regardless.
--strict-plugins elevates any non-valid plugin row or non-empty
runtime_errors (and, v0.20+, the same for third-party adapters) to exit code
4. Default behavior is lenient — failures are recorded and the scan proceeds.
Plugins cannot declare
dynamic_default=True. Dynamic-severity checks (whose emitted severity depends on manifest values) must wire into the coredynamic_defaultsaggregator, which plugins have no path into — so the validation pipeline rejects them withdynamic_default_not_supported. Declare a fixeddefault_severity(plus an optionalfloor_severity) instead.
Don't reinvent risk classification. Import from agents_shipgate.core.risk_hints:
from agents_shipgate.core.risk_hints import (
has_risk_tag, # check tag presence at minimum confidence
risk_tags, # sorted list of tags at min confidence
is_effectively_read_only,
is_high_risk_tool,
is_write_tool,
)These guard you against substring false positives that the built-in classifier already addresses (it tokenizes names/descriptions/scopes rather than substring-matching).
Always start with the cheapest check that excludes most tools:
for tool in context.tools:
if is_effectively_read_only(tool):
continue
if not has_risk_tag(tool, {"sensitive_data_access"}, min_confidence="medium"):
continue
# … expensive checks hereEvidence keys go into the fingerprint. Pick names that won't change between runs:
evidence={
"risk_tags": risk_tags(tool, min_confidence="medium"), # sorted
"annotation_present": False, # explicit bool
"policy_match": None, # explicit None vs missing
}Avoid putting timestamps, absolute paths, or run-specific data into evidence — fingerprints will rotate and baselines will break.
A plugin's job is to add findings, not to crash the scan. If something unexpected happens, return an empty list and log via logging.getLogger(__name__).warning(...) — Shipgate's structured logger will pick it up under AGENTS_SHIPGATE_LOG_FORMAT=json.
The repo ships sample agent fixtures under samples/. Add your plugin to a test environment and run:
agents-shipgate scan -c samples/support_refund_agent/shipgate.yamlPin expected findings in your own test suite so manifest schema changes upstream don't silently break your check.
| Shipgate version | What it guarantees for plugins |
|---|---|
>=0.10,<0.11 |
The contracts above are stable. ScanContext, Finding, CheckMetadata, and the tool_finding/agent_finding helpers will not change in a breaking way within the pinned minor. |
| Next minor | Likely additive: new context fields, new finding fields. Breaking changes are flagged in release notes. |
Pin shipgate carefully in your plugin's pyproject.toml. The runtime ABI is the
public Pydantic models under agents_shipgate.schemas.* (and the
agents_shipgate.core.context/agents_shipgate.checks.base import paths used
above); field changes surface as ValidationError at import time. Do not
import from other internal agents_shipgate.core.* modules — those are not a
stable consumer API.
- Modify the manifest in place
- Replace built-in checks
- Suppress other plugins' findings programmatically (use
checks.ignoreinstead) - Reach the file system outside whatever the user already declared in
tool_sources
If you find yourself needing one of these, that's usually a sign the requirement belongs in shipgate core — please open an issue.
Agents Shipgate · Apache-2.0 · maintained by Three Moons Lab · 2026 tool-surface study · Report a false positive
Getting started
Reference
Workflows
Research
Extending
Project