Skip to content

Trust Model

Pengfei Hu edited this page May 31, 2026 · 2 revisions

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.


By default, the scanner does not

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 fetchverify 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.

Audited exceptions are pinned, not promised

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.

Input safety

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 resolved

A 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 $ref resolution: MAX_SCHEMA_RESOLVE_DEPTH = 32, MAX_SCHEMA_RESOLVE_NODES = 5000 (truncated with an x-agents-shipgate-resolution-truncated marker rather than crashing)

YAML is parsed with yaml.safe_load only. The unsafe !!python/object/... constructor is rejected.

Static Python extraction (SDK, ADK, LangChain, CrewAI)

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_override and description_override from 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.

Secrets never echo to reports

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.


Plugin trust boundary

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 only

When plugins ARE loaded:

  1. Only entry points from distributions other than agents-shipgate itself are considered (registry.py:_is_builtin_entry_point checks dist.metadata["Name"]).
  2. 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"
      }
    ]
  3. 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.


Verifying these claims

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 grep for subprocess/os.system now returns hits in the audited meta-CLI surfaces (verify/trigger git probes, bootstrap, self-check). That's expected — test_adapter_static_only.py is the authoritative gate, because it distinguishes the parsing path (which must be execution-free) from those pinned exceptions, none of which run user code.


Distribution integrity

Releases are:

  • Built reproducibly via python -m build
  • Signed with sigstore (sigstore sign) — .sig and .crt artifacts 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

What we do not promise

  • 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.

Clone this wiki locally