-
Notifications
You must be signed in to change notification settings - Fork 0
Trust Model
Agents Shipgate is the deterministic merge gate for AI-generated agent capability changes — and the trust property that makes its verdict worth trusting is that it is static by default: it never runs the agent it is reviewing. When a coding agent (Claude Code, Codex, Cursor) or a human changes what an AI agent can do, Shipgate turns the PR diff into a deterministic merge verdict without executing, importing, or phoning home. What Shipgate does and does not do, by design, is verifiable from the source.
Every adapter — MCP, OpenAPI, OpenAI Agents SDK, Anthropic Messages API, Google ADK, LangChain/LangGraph, CrewAI, OpenAI API, Codex plugin packages, and n8n workflows — is static-only.
| Run agent code or import user modules | All framework adapters (SDK, ADK, LangChain, CrewAI) use ast.parse only |
| Call tools | No HTTP client is ever instantiated for tool invocation |
| Invoke LLMs | The risk classifier is rule-based, not model-based |
| Connect to MCP servers | MCP loading is JSON-export only |
| Make network calls | All inputs are local files; the scanner never fetches (not even git fetch — verify requires the base ref made available beforehand) |
| Read files outside the manifest directory | Path traversal blocked; see Input safety |
| Collect telemetry | Not even anonymous usage stats |
These properties hold unless you opt into plugins.
The no-execute / no-import property is enforced on every CI run by an AST scan of
every .py file under src/agents_shipgate/
(tests/test_adapter_static_only.py),
not by convention. The scan forbids exec/eval/__import__/compile, dynamic
importlib/runpy loading, and subprocess/os.system/os.exec*/os.spawn*
call sites. The handful of first-party meta-CLI surfaces that legitimately shell
out — e.g. verify/trigger reading local git metadata, bootstrap chaining
the installed CLI, self-check import-probing supplied modules — are pinned
per call site in ALLOWED_EXCEPTIONS by a (path, surface, line, snippet)
four-tuple. Adding a second call to an already-allowlisted surface, or changing a
pinned call's argv shape, fails the test until a reviewer confirms it. None of
these touch the scanner's parsing path or run user code.
A complementary per-adapter test
(tests/test_fixture_no_import.py)
drives each adapter against a fixture whose Python content raises at module load
and asserts no module under the fixture root ends up in sys.modules after the
scan.
Every path declared in shipgate.yaml (under tool_sources, openai_api, anthropic, google_adk, langchain, crewai, n8n, validation, etc.) is resolved through inputs/common.py:resolve_input_path:
def resolve_input_path(base_dir: Path, value: str) -> Path:
base = base_dir.resolve()
raw_path = Path(value)
path = raw_path if raw_path.is_absolute() else base / raw_path
resolved = path.resolve()
try:
resolved.relative_to(base)
except ValueError as exc:
raise InputParseError(
f"Input path {value!r} resolves outside manifest directory: {resolved}"
) from exc
return resolvedA malicious manifest with path: ../../../../etc/passwd is rejected with InputParseError (exit code 3). Sibling-directory specs must be moved into the manifest tree, symlinked in, or copied in during CI prep — out-of-tree paths are rejected by containment.
Files are also size-bounded:
-
MAX_INPUT_FILE_BYTES = 10 MB(common.py) - OpenAPI
$refresolution:MAX_SCHEMA_RESOLVE_DEPTH = 32,MAX_SCHEMA_RESOLVE_NODES = 5000(truncated with anx-agents-shipgate-resolution-truncatedmarker rather than crashing)
YAML is parsed with yaml.safe_load only. The unsafe !!python/object/... constructor is rejected.
The openai_agents_sdk, google_adk, langchain, and crewai source types read
Python files via ast.parse and walk the tree for tool definitions. They never
import the file or framework packages. For the OpenAI Agents SDK adapter,
practically:
- It detects direct decorator names (
@function_tool,@agents.function_tool,@openai_agents.function_tool). - It detects renamed imports (
from agents import function_tool as ft→ recognizes@ft). - It reads
name_overrideanddescription_overridefrom decorator kwargs. - It cannot detect dynamic wrappers, factory-built tools, runtime imports, or tools added to a list.
The OpenAI Agents SDK, CrewAI, and LangChain/LangGraph extractors share one
runtime/context parameter skip list (self, cls, ctx, context, config,
runtime, run_manager, callbacks); Google ADK uses its own (self, ctx,
context, tool_context). Those framework-plumbing parameters are omitted from
normalized tool input schemas.
For production targets, this medium-confidence extraction triggers SHIP-INVENTORY-LOW-CONFIDENCE-PRODUCTION-SURFACE — your nudge to migrate the inventory to MCP or OpenAPI declarations. See Real-World Examples § OpenAI Agents SDK.
SHIP-DOC-SECRET-IN-DESCRIPTION matches patterns like sk-…, ghp_…, AKIA…, and password|secret|token|api_key: ${value}. The report records the pattern that matched (e.g. \\bsk-[A-Za-z0-9_-]{16,}), never the actual value. The SHIP-DOC-INJECTION-RISK finding similarly records the pattern, not the description text.
This is verified by tests/test_documentation_checks.py. If you see an actual secret leak into a report, that's a bug — please file a security advisory at SECURITY.md.
Third-party check plugins are the only opt-in escape from the static-by-default guarantee. Plugins are arbitrary Python code that gets imported and executed when:
AGENTS_SHIPGATE_ENABLE_PLUGINS=1 agents-shipgate scan ...The runtime overrides:
agents-shipgate scan --no-plugins # forces off, even if env is set
agents-shipgate list-checks --no-plugins # catalog without plugin entries
agents-shipgate explain --no-plugins SHIP-X # built-ins onlyWhen plugins ARE loaded:
- Only entry points from distributions other than
agents-shipgateitself are considered (registry.py:_is_builtin_entry_pointchecksdist.metadata["Name"]). - Each loaded plugin appears in the report:
"loaded_plugins": [ { "name": "compliance", "value": "acme_shipgate_checks.compliance:run", "distribution": "acme-shipgate-checks", "version": "1.2.3", "check_id": "ACME-COMPLIANCE-PII-MASKING" } ]
- Reviewers can see exactly which third-party packages contributed checks.
Operational guidance. Treat plugins like other CI dependencies: pin versions, audit transitive dependencies, and avoid enabling plugins in untrusted-input contexts unless those packages are approved by your security team. See Plugin Authoring for the contract.
The trust model is enforceable from the code. To audit:
| Claim | How to verify |
|---|---|
| Scanner parsing path is execution-free; every meta-CLI exception is pinned |
pytest tests/test_adapter_static_only.py (AST scan of all of src/agents_shipgate/ + the ALLOWED_EXCEPTIONS per-call-site allowlist) |
| Adapters never import the files they scan |
pytest tests/test_fixture_no_import.py (per-adapter load-time trap + sys.modules snapshot) |
| No network in scanner |
grep -rn "requests\|urllib\|httpx\|httplib\|aiohttp" src/ returns nothing |
| No telemetry |
grep -rn "posthog\|mixpanel\|sentry\|telemetry" src/ returns nothing |
| YAML safe_load only |
grep -rn "yaml.load\|yaml.unsafe" src/ returns nothing |
| Path traversal rejected | pytest tests/test_inputs.py::test_mcp_loader_rejects_path_traversal |
| Plugin builtin spoof rejected | pytest tests/test_plugins.py::test_builtin_distribution_entry_points_are_skipped |
Note: a plain
grepforsubprocess/os.systemnow returns hits in the audited meta-CLI surfaces (verify/triggergit probes,bootstrap,self-check). That's expected —test_adapter_static_only.pyis the authoritative gate, because it distinguishes the parsing path (which must be execution-free) from those pinned exceptions, none of which run user code.
Releases are:
- Built reproducibly via
python -m build - Signed with sigstore (
sigstore sign) —.sigand.crtartifacts ship with the release - Published to PyPI via trusted publishing (OIDC; no long-lived credentials)
- Accompanied by a CycloneDX SBOM (
agents-shipgate-sbom.json) attached to the GitHub release
Verify the signature on a downloaded artifact (substitute the release you pulled):
sigstore verify identity \
--bundle agents_shipgate-0.10.0.tar.gz.sigstore \
--cert-identity 'https://github.com/ThreeMoonsLab/agents-shipgate/.github/workflows/release.yml@refs/tags/v0.10.0' \
--cert-oidc-issuer 'https://token.actions.githubusercontent.com' \
agents_shipgate-0.10.0.tar.gz- We don't certify agent safety or compliance. Findings are evidence; the release decision belongs to you.
- We don't catch every risky tool. Substring-named risks, dynamically-built tool surfaces, and runtime-only behavior all live outside the static analysis surface.
- We don't fix findings. Recommendations are suggestions for the reviewer, not prescriptions.
If you find a check that's wrong (false positive or false negative), please open an issue tagged false-positive or false-negative — that's how the catalog improves.
Agents Shipgate · Apache-2.0 · maintained by Three Moons Lab · Report a false positive
Getting started
Reference
Workflows
Extending
Project