Skip to content

Guided tour

Six things people actually need from a fleet they did not build, answered against one fixture, in the order the questions arrive. Each story is one or two commands and what its output means.

Before you start

Everything here is offline except pacto dashboard. No cluster, no registry, no running service and no network — the fixture is committed to the repository, so it works on a plane.

You need the Pacto CLI on your PATH and a clone:

git clone https://github.com/TrianaLab/pacto.git && cd pacto

Every command below is written to be pasted from the repository root.

To drive a real stack instead, see the Docker Compose demo, which needs no clone and no CLI.

The tour shows runtime conformance (checking what a service really does on the network) as a recorded finding only: story 2 shows one the kernel saw. To watch it live, use the Compose demo or the node agent on a Kubernetes cluster.

The fleet

Sixteen services are in the snapshot. Five carry the stories:

Service Owner Its part
payments-service payments-team six revisions; the change stories 3 and 4 analyse
orders-service commerce-team declares a dependency on payments, and is observed calling it
api-gateway platform-foundations declares the same dependency, and has an expired readiness assessment
auth-service identity-team deployed, but never observed — the Unknown target
audit-log platform-foundations-security calls payments and declares nothing — the shadow consumer

A contract says what a service exposes, what it needs and how it behaves — never how it is deployed. The contract reference has every section.

Story 1 — "I inherited this fleet and I do not know what is in it"

Enumerate everything from contracts alone, then find out what state it is in.

pacto fleet search --local examples/demo/bundles
16 of 16 service(s):
  api-gateway                  NotEvaluated owner=platform-foundations revs=3 targets=0
  audit-log                    NotEvaluated owner=platform-foundations-security revs=1 targets=0
  auth-service                 NotEvaluated owner=identity-team  revs=1 targets=0
  email-provider               NotEvaluated owner=-              revs=1 targets=0
  fraud-service                NotEvaluated owner=payments-team  revs=1 targets=0
  frontend                     NotEvaluated owner=frontend-team  revs=1 targets=0
  notification-worker          NotEvaluated owner=commerce-team  revs=1 targets=0
  orders-service               NotEvaluated owner=commerce-team  revs=3 targets=0
  pacto-demo                   NotEvaluated owner=platform-foundations revs=1 targets=0
  payments-service             NotEvaluated owner=payments-team  revs=6 targets=0
  platform-app-config          NotEvaluated owner=platform-foundations revs=1 targets=0
  platform-http-policy         NotEvaluated owner=platform-foundations revs=1 targets=0
  platform-worker-config       NotEvaluated owner=platform-foundations revs=1 targets=0
  postgresql                   NotEvaluated owner=infra-data     revs=1 targets=0
  redis                        NotEvaluated owner=infra-data     revs=1 targets=0
  stripe-api                   NotEvaluated owner=external/stripe revs=1 targets=0

Sixteen services, each with its owner, how many contract revisions it has published and how many deployed targets are running it. NotEvaluated is not a verdict: no evidence source is wired up, so nothing has been evaluated.

--target-state folds in what a platform observed about the running deployments. Three of its targets, abridged:

schemaVersion: pacto.dev/fleet-targets/v1
targets:
  - { scope: production-eu, kind: kubernetes-workload, name: payments/payments-service,
      service: payments-service, compliance: Compliant,
      coverage: { evaluated: 6, required: 6 }, evidenceAt: 2026-07-29T09:40:00Z }

  - scope: production-eu
    kind: kubernetes-workload
    name: commerce/orders-service
    service: orders-service
    compliance: NonCompliant
    coverage: { evaluated: 5, required: 5 }
    evidenceAt: 2026-07-29T09:40:00Z
    findings:
      - code: STATELESS_PERSISTENT_CONFLICT
        severity: error
        category: RuntimeDrift
        message: declared stateless but the observed workload mounts a persistent volume
      - code: INTERFACE_ABSENT
        severity: error
        category: RuntimeDrift
        message: nothing listens on the declared http interface
        evidenceRefs: [{ source: k8s, collector: k8s-observer+ebpf }]

  - { scope: production-eu, kind: kubernetes-workload, name: identity/auth-service,
      service: auth-service, compliance: Unknown,
      coverage: { evaluated: 1, required: 4 } }
pacto fleet search --local examples/demo/bundles --target-state examples/demo/fleet-targets.yaml
16 of 16 service(s):
  api-gateway                  NotEvaluated owner=platform-foundations revs=3 targets=0
  audit-log                    NotEvaluated owner=platform-foundations-security revs=1 targets=0
  auth-service                 Unknown      owner=identity-team  revs=1 targets=1
  email-provider               NotEvaluated owner=-              revs=1 targets=0
  fraud-service                Compliant    owner=payments-team  revs=1 targets=1
  frontend                     NotEvaluated owner=frontend-team  revs=1 targets=0
  notification-worker          NotEvaluated owner=commerce-team  revs=1 targets=0
  orders-service               NonCompliant owner=commerce-team  revs=3 targets=1
  pacto-demo                   NotEvaluated owner=platform-foundations revs=1 targets=0
  payments-service             Compliant    owner=payments-team  revs=6 targets=1
  platform-app-config          NotEvaluated owner=platform-foundations revs=1 targets=0
  platform-http-policy         NotEvaluated owner=platform-foundations revs=1 targets=0
  platform-worker-config       NotEvaluated owner=platform-foundations revs=1 targets=0
  postgresql                   NotEvaluated owner=infra-data     revs=1 targets=0
  redis                        NotEvaluated owner=infra-data     revs=1 targets=0
  stripe-api                   NotEvaluated owner=external/stripe revs=1 targets=0

Four states in one screen. Compliant is a verdict backed by evidence, NonCompliant is a confirmed contradiction, Unknown is evidence that never arrived and NotEvaluated means nobody is deployed there to evaluate.

Story 2 — "Something says Unknown and I need to know whether that is bad"

Get the reason behind a verdict, so absence of evidence never reads as a pass.

pacto fleet explain identity/auth-service --local examples/demo/bundles --target-state examples/demo/fleet-targets.yaml --freshness 24h
target production-eu/kubernetes-workload/identity%2Fauth-service: Unknown
  [EVIDENCE_MISSING] no evidence has been observed for this target
pacto explain examples/demo/bundles/api-gateway/v1.2.0
Service: api-gateway@1.2.0
Owner: platform-foundations
Pacto Version: 2.0

Workload: service

State:
  Type: stateless
  Persistence: local/ephemeral
  Data Criticality: low

Interfaces (1):
  - http (openapi: interfaces/openapi.json, public)

Dependencies (3):
  - auth-service: oci://ghcr.io/trianalab/pacto/auth-service (^1.0.0, required)
  - orders-service: oci://ghcr.io/trianalab/pacto/orders-service (^1.0.0, required)
  - payments-service: oci://ghcr.io/trianalab/pacto/payments-service (^1.0.0, required)

Readiness:
  Score: 0
  Gate: FAIL (score 0 / minScore 75)
  Earned Weight: 0
  Total Weight: 150
  Expires: 2025-01-01 (EXPIRED)
  Status: 5 done, 0 partial, 0 not-done, 0 deferred

  Checks:
    - gateway-dashboard observability  done      weight=30 earned=0 evidence=https://grafana.acme.com/d/api-gateway
    - rate-limiting    infrastructure done      weight=25 earned=0 evidence=ENG-1234
    - security-review  security       done      weight=40 earned=0 evidence=SEC-2045
    - load-test        testing        done      weight=35 earned=0 evidence=https://k6.acme.com/api-gateway-load-2024-12
    - runbook          documentation  done      weight=20 earned=0 evidence=docs/runbooks/api-gateway.md

EVIDENCE_MISSING is a fact about the observer, not about the service. api-gateway declares five readiness claims, every one marked done with evidence attached, and the assessment earns zero because it expired on 2025-01-01. Pacto fails closed on both.

A confirmed violation reads differently:

pacto fleet get --target commerce/orders-service --local examples/demo/bundles --target-state examples/demo/fleet-targets.yaml
Target: production-eu/kubernetes-workload/commerce%2Forders-service
Service: orders-service
Compliance: NonCompliant
Revision: orders-service@sha256:abf75f569260779a932066089e72cf0c2e6c8ac61a22fcf849cced91aaa75736 (inferred)
Stale: false
Coverage: 5/5 evaluated
  finding [error] STATELESS_PERSISTENT_CONFLICT: declared stateless but the observed workload mounts a persistent volume
  finding [error] INTERFACE_ABSENT: nothing listens on the declared http interface (seen by k8s-observer+ebpf)

Coverage: 5/5 evaluated is what separates this from the Unknown above: every check ran, so the verdict is a conclusion rather than a gap. Each finding names one exact contradiction. The contract declares the workload stateless and the observation found a persistent volume mounted. The contract declares an http interface and nothing listens on its port: seen by k8s-observer+ebpf says the node agent saw that in the kernel, and Kubernetes agreed. One fact disagreeing with one declaration, not a score.

Story 3 — "I am about to ship a change that might break someone"

Classify a change from two contracts, with nothing running, then gate CI on it.

pacto diff examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1
Classification: BREAKING
Changes (39):
  [NON_BREAKING] service.version (modified): service.version modified [1.2.1 -> 2.0.1]
  [POTENTIAL_BREAKING] capabilities (removed): capabilities removed [- extension (payments.trianalab.com/refund)]
  [NON_BREAKING] capabilities (added): capabilities added [+ extension (payments.trianalab.com/dispute)]
  [POTENTIAL_BREAKING] dependencies.required (modified): dependencies.required modified [fraud-service: required=false -> fraud-service: required=true]
  [BREAKING] asyncapi.channels[payment.completed] (removed): channel payment.completed removed [- payment.completed]
  [BREAKING] asyncapi.channels[payment.failed] (removed): channel payment.failed removed [- payment.failed]
  [POTENTIAL_BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.properties.charge_id (removed): asyncapi.channels[payment.refunded].publish.message.payload.properties.charge_id removed [- map[type:string]]
  [POTENTIAL_BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.properties.payment_intent_id (added): asyncapi.channels[payment.refunded].publish.message.payload.properties.payment_intent_id added [+ map[type:string]]
  [BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.required[payment_intent_id] (added): asyncapi.channels[payment.refunded].publish.message.payload.required payment_intent_id added [+ payment_intent_id]
  [BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.required[charge_id] (removed): asyncapi.channels[payment.refunded].publish.message.payload.required charge_id removed [- charge_id]
  [NON_BREAKING] asyncapi.channels[payment.intent.created] (added): channel payment.intent.created added [+ payment.intent.created]
  [NON_BREAKING] asyncapi.channels[payment.intent.failed] (added): channel payment.intent.failed added [+ payment.intent.failed]
  [NON_BREAKING] asyncapi.channels[payment.intent.succeeded] (added): channel payment.intent.succeeded added [+ payment.intent.succeeded]
  [BREAKING] openapi.paths[/charges] (removed): API path /charges removed [- /charges]
  [BREAKING] openapi.paths[/charges/{id}] (removed): API path /charges/{id} removed [- /charges/{id}]
  [POTENTIAL_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.charge_id (removed): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.charge_id removed [- map[type:string]]
  [POTENTIAL_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.payment_intent_id (added): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.payment_intent_id added [+ map[type:string]]
  [BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required[payment_intent_id] (added): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required payment_intent_id added [+ payment_intent_id]
  [NON_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required[charge_id] (removed): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required charge_id removed [- charge_id]
  [POTENTIAL_BREAKING] openapi.paths[/refunds].methods[POST].responses[404].description (modified): openapi.paths[/refunds].methods[POST].responses[404].description changed [Charge not found -> Payment intent not found]
  [POTENTIAL_BREAKING] openapi.paths[/webhooks/stripe].methods[POST].request-body (removed): POST /webhooks/stripe request body removed [- POST /webhooks/stripe]
  [POTENTIAL_BREAKING] openapi.paths[/webhooks/stripe].methods[POST].responses[400].description (modified): openapi.paths[/webhooks/stripe].methods[POST].responses[400].description changed [Invalid webhook signature -> Invalid signature]
  [NON_BREAKING] openapi.paths[/payment-intents] (added): API path /payment-intents added [+ /payment-intents]
  [NON_BREAKING] openapi.paths[/payment-intents/{id}] (added): API path /payment-intents/{id} added [+ /payment-intents/{id}]
  [NON_BREAKING] openapi.paths[/payment-intents/{id}/cancel] (added): API path /payment-intents/{id}/cancel added [+ /payment-intents/{id}/cancel]
  [NON_BREAKING] openapi.paths[/payment-intents/{id}/confirm] (added): API path /payment-intents/{id}/confirm added [+ /payment-intents/{id}/confirm]
  [POTENTIAL_BREAKING] schema.properties.ENABLE_REFUNDS (removed): schema.properties.ENABLE_REFUNDS removed [- map[default:true description:Feature flag to enable/disable refund processing type:boolean]]
  [POTENTIAL_BREAKING] schema.properties.FRAUD_CHECK_ENABLED.default (modified): schema.properties.FRAUD_CHECK_ENABLED.default changed [false -> true]
  [POTENTIAL_BREAKING] schema.properties.FRAUD_CHECK_ENABLED.description (modified): schema.properties.FRAUD_CHECK_ENABLED.description changed [Enable fraud detection checks before processing payments -> Fraud detection is now enabled by default and required]
  [POTENTIAL_BREAKING] schema.properties.STRIPE_API_KEY (removed): schema.properties.STRIPE_API_KEY removed [- map[description:Stripe API secret key for payment processing type:string]]
  [POTENTIAL_BREAKING] schema.properties.STRIPE_SECRET_KEY (added): schema.properties.STRIPE_SECRET_KEY added [+ map[description:Stripe secret key (renamed from STRIPE_API_KEY) type:string]]
  [POTENTIAL_BREAKING] schema.properties.WEBHOOK_SECRET.description (modified): schema.properties.WEBHOOK_SECRET.description changed [Stripe webhook signing secret for signature verification -> Stripe webhook signing secret (now required)]
  [BREAKING] schema.required[STRIPE_SECRET_KEY] (added): schema.required STRIPE_SECRET_KEY added [+ STRIPE_SECRET_KEY]
  [BREAKING] schema.required[WEBHOOK_SECRET] (added): schema.required WEBHOOK_SECRET added [+ WEBHOOK_SECRET]
  [BREAKING] schema.required[STRIPE_API_KEY] (removed): schema.required STRIPE_API_KEY removed [- STRIPE_API_KEY]
  [NON_BREAKING] configurations[app].values.ENABLE_REFUNDS (removed): configurations[app].values.ENABLE_REFUNDS removed [- true]
  [NON_BREAKING] configurations[app].values.FRAUD_CHECK_ENABLED (modified): configurations[app].values.FRAUD_CHECK_ENABLED changed [false -> true]
  [NON_BREAKING] configurations[app].values.STRIPE_API_KEY (removed): configurations[app].values.STRIPE_API_KEY removed [- ${STRIPE_API_KEY}]
  [NON_BREAKING] configurations[app].values.STRIPE_SECRET_KEY (added): configurations[app].values.STRIPE_SECRET_KEY added [+ ${STRIPE_SECRET_KEY}]

SBOM changes (1):
  ~ stripe-go version: 1.0.0 -> 2.0.0
breaking changes detected

Thirty-nine changes, one verdict and a non-zero exit so CI can gate on it. Two API paths removed, two event channels withdrawn, a required request field swapped for a differently-named one, two configuration keys becoming required. A capability dropped, an optional dependency became mandatory and one SBOM package version moved. All of it is one contract compared with another — no running service was consulted. The classification rules are a published table, not a heuristic.

Not every release is a break. The same service, one pair earlier:

pacto diff examples/demo/bundles/payments-service/v1.0.0 examples/demo/bundles/payments-service/v1.1.0
Classification: POTENTIAL_BREAKING
Changes (6):
  [NON_BREAKING] service.version (modified): service.version modified [1.0.0 -> 1.1.0]
  [NON_BREAKING] asyncapi.channels[payment.failed] (added): channel payment.failed added [+ payment.failed]
  [POTENTIAL_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.amount.description (modified): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.amount.description changed [Refund amount in cents (partial refund if less than charge) -> Refund amount in cents]
  [NON_BREAKING] openapi.paths[/webhooks/stripe] (added): API path /webhooks/stripe added [+ /webhooks/stripe]
  [POTENTIAL_BREAKING] schema.properties.WEBHOOK_SECRET (added): schema.properties.WEBHOOK_SECRET added [+ map[description:Stripe webhook signing secret for signature verification type:string]]
  [NON_BREAKING] configurations[app].values.WEBHOOK_SECRET (added): configurations[app].values.WEBHOOK_SECRET added [+ ${STRIPE_WEBHOOK_SECRET}]

POTENTIAL_BREAKING, not BREAKING: a new endpoint, a new event channel and a new optional configuration property break nobody by themselves, but adding a property to a schema can still surprise a consumer that validates strictly. Three classifications exist because two would force every additive change into one of the wrong ones.

Story 4 — "It does break. Who do I have to tell?"

Turn a classification into a list of consumers, each graded by how it is known.

pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 10
Potentially breaking changes: 15
Affected consumers (4):
  api-gateway                  direct     confidence=contractual  compat=incompatible owner=platform-foundations
  frontend                     transitive confidence=inferred     compat=unknown      owner=frontend-team
  orders-service               direct     confidence=contractual  compat=incompatible owner=commerce-team
  pacto-demo                   transitive confidence=inferred     compat=unknown      owner=platform-foundations

Four consumers. confidence=contractual means the consumer declared this dependency in its own contract; confidence=inferred means it was reached transitively. compat=incompatible is a second judgement: the consumer's declared version range does not admit 2.0.1.

Contracts only find consumers that wrote one down. Add observed traffic:

pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles --traces examples/demo/traces.json
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 10
Potentially breaking changes: 15
Affected consumers (5):
  api-gateway                  direct     confidence=contractual  compat=incompatible owner=platform-foundations
  audit-log                    direct     confidence=observed     compat=unknown      owner=platform-foundations-security
  frontend                     transitive confidence=inferred     compat=unknown      owner=frontend-team
  orders-service               direct     confidence=corroborated compat=incompatible owner=commerce-team
  pacto-demo                   transitive confidence=inferred     compat=unknown      owner=platform-foundations

Five now. audit-log calls payments-service in production and declares nothing, so no amount of reading contracts would ever have found it — it arrives as confidence=observed. orders-service, which both declared the dependency and was seen using it, is upgraded to confidence=corroborated.

Then ask where the change actually lands:

pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles --traces examples/demo/traces.json --target-state examples/demo/fleet-targets.yaml
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 10
Potentially breaking changes: 15
Affected consumers (5):
  api-gateway                  direct     confidence=contractual  compat=incompatible owner=platform-foundations
  audit-log                    direct     confidence=observed     compat=unknown      owner=platform-foundations-security
  frontend                     transitive confidence=inferred     compat=unknown      owner=frontend-team
  orders-service               direct     confidence=corroborated compat=incompatible owner=commerce-team
  pacto-demo                   transitive confidence=inferred     compat=unknown      owner=platform-foundations
Active targets (2): [production-eu/kubernetes-workload/commerce%2Forders-service production-eu/kubernetes-workload/payments%2Fpayments-service]
breaking changes affect active consumers

The consumer list is identical. What is new is the last two lines. Two deployed targets sit in the path of this change — orders-service, one of the five consumers, and payments-service itself — so this is not hypothetical, and the command exits non-zero. Story 3 refused a change for what it is. This refuses it for where it lands.

Story 5 — "My architecture diagram and my traffic disagree"

Reconcile the edges contracts declare against the edges something observed.

pacto fleet reconcile --local examples/demo/bundles --traces examples/demo/traces.json
Reconciliation: 1 matched, 13 declared-not-observed, 1 observed-not-declared
  [declared-not-observed] api-gateway -> auth-service (count=0)
  [declared-not-observed] api-gateway -> orders-service (count=0)
  [declared-not-observed] api-gateway -> payments-service (count=0)
  [observed-not-declared] audit-log -> payments-service (count=1)
  [declared-not-observed] auth-service -> redis (count=0)
  [declared-not-observed] fraud-service -> redis (count=0)
  [declared-not-observed] frontend -> api-gateway (count=0)
  [declared-not-observed] notification-worker -> email-provider (count=0)
  [declared-not-observed] orders-service -> notification-worker (count=0)
  [matched] orders-service -> payments-service (count=1)
  [declared-not-observed] orders-service -> postgresql (count=0)
  [declared-not-observed] pacto-demo -> frontend (count=0)
  [declared-not-observed] payments-service -> fraud-service (count=0)
  [declared-not-observed] payments-service -> postgresql (count=0)
  [declared-not-observed] payments-service -> stripe-api (count=0)

Three verdicts. observed-not-declared is the shadow dependency from story 4, stated as a reconciliation result. declared-not-observed is the opposite risk — an edge in the diagram that no traffic has ever taken.

Story 6 — "I want an agent on this, without handing it write access"

Serve a contract as MCP tools, and check what the default withholds.

pacto mcp turns a bundle's OpenAPI interface into MCP tools. By default it turns only the safe half. Point a human and an agent at one process:

pacto dashboard examples/demo/bundles --port 8899

Then open http://127.0.0.1:8899/#/fleet. The dashboard is itself a Pacto bundle with a real OpenAPI contract, so the agent's tools are generated from the contract of the very server the human is looking at. The tools come from the dashboard's OpenAPI interface, one per read-only operation. Beside the interface operations the server always registers Pacto's own authoring tools — pacto_create and pacto_edit write contract files to disk.

Mutating operations are withheld by default, in every bundle. Serve the demo's payments service instead, and its five mutating operations — every POST, PUT, PATCH and DELETE that bundle's interface declares — are dropped with a warning on stderr:

pacto mcp examples/demo/bundles/payments-service/v2.1.0 --base-url http://127.0.0.1:1

The base URL is deliberately a dead address: the tools are registered and listed without anything ever being called.

Add --allow-writes and both the warning and the restriction disappear. --allow-writes governs the interface half only; withhold the authoring tools by not registering this server where you do not want contracts written. See MCP integration for the full wiring.

Next

The Quickstart takes an empty directory to a validated contract and a caught breaking change in about five minutes. The contract reference is the complete list of sections, and pacto diff is the complete table of what counts as a breaking change.