> For the complete documentation index, see [llms.txt](https://fleet.hackmap.win/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fleet.hackmap.win/concepts/contracts.md).

# Contracts

A contract is the schema that two automations agree on so they can talk to each other. Fleet has three contract kinds: pools, queues, and streams. Each is a typed reference — a name plus one or two Pydantic models — declared exactly once and imported wherever it's used.

## The three kinds

`Pool[P, T]` is a shared bag of items. Producers `put`. Consumers `claim` or `list`. The framework persists items with TTL, supports tag-based filtering, and handles exclusive claims with auto-release. `P` is the payload model; `T` is the tag model (the queryable slice).

`Queue[P]` is a one-to-one work pipe for `BatchAutomation` types. The producer calls `submit`; the receiver's slots `BLPOP` from it. `P` is the task payload model.

`Stream[P]` is the typed reader for an automation's output channel. Every automation that declares `Output = SomeModel` automatically has a stream named after itself. Consumers reach for it through `ctx.stream(SOME_STREAM_REF)`. `P` is the payload model.

## Where contracts live

Pools that an automation owns are declared inline on the automation class:

```python
class CookieHarvester(BatchAutomation[HarvesterConfig]):
    TaskPayload = HarvestRequest
    Pools = {
        "cookies": Pool[CfCookie, CfCookieTags](
            "cf-cookies", payload=CfCookie, tags=CfCookieTags,
        ),
    }
```

Stream and queue references for an automation are derived from `Output` and `TaskPayload`. The framework builds them automatically and exposes them through the catalog.

## Declared resources

Beyond the contract kinds above, automations can declare runtime *resources* the framework should provision at worker boot. The pattern is the same — a ClassVar on the automation, read by the worker bootstrap. Today the only declared resource is `Browser`:

```python
from fleet.core.browser import BrowserNeeds

class GoogleSerpAutomation(BatchAutomation[GoogleConfig]):
    TaskPayload = SerpQuery
    Output = SerpResult
    Browser = BrowserNeeds(size=4, max_uses=1, engine="cloak")

    async def run_one(self, task, ctx):
        async with ctx.browser.acquire(proxy=ctx.proxy_url) as page:
            ...
```

The worker boots one `BrowserResource` (a pre-warmed `BrowserPool`) per unique `BrowserNeeds` across all loaded automations and injects it as `ctx.browser`. Plugins never `import fleet_browser`. See [browser-based automations](/guides/browser-automations.md) for the full reference.

When two plugins both need the same `Pool` or `Queue` reference, the cleanest place to put it is a small shared package. By convention, name it `<thing>-contracts`:

```python
# cf_contracts/__init__.py
from pydantic import BaseModel
from fleet.core import Pool, Queue, Stream

class CfCookie(BaseModel):
    cookies: dict[str, str]
    user_agent: str
    proxy_url: str | None
    site: str

class CfCookieTags(BaseModel):
    site: str
    fresh: bool = True

class HarvestRequest(BaseModel):
    site: str

class ScrapeResult(BaseModel):
    url: str
    status: int
    body_len: int

COOKIE_POOL = Pool[CfCookie, CfCookieTags]("cf-cookies", payload=CfCookie, tags=CfCookieTags)
HARVESTER   = Queue[HarvestRequest]("cf-cookie-harvester", payload=HarvestRequest)
SCRAPER_OUT = Stream[ScrapeResult]("cf-scraper", payload=ScrapeResult)
```

Both `cf-cookie-harvester` and `cf-scraper` pip-depend on `cf-contracts` and import these references. The package has no runtime code; it ships Pydantic models and string constants. That's it.

## Why a separate package?

Three reasons that keep coming up:

The producer plugin is the source of truth for its own schemas, but other plugins shouldn't have to install the producer's runtime to read its types. `cf-scraper` shouldn't pull in DrissionPage and Chromium just to know what a `CfCookie` looks like. Splitting types out makes the consumer's dependency footprint match its actual needs.

Renames stay local. If you decide `cf-cookies` should become `cf-clearance-bundles`, you change the string in one constant in `cf-contracts` and bump the package version. Both plugins pin against the new version and recompile clean. Without the shared package, the rename touches every consumer.

Static typing works without codegen. The consumer imports `CfCookie` from the contracts package and the IDE knows `item.payload.cookies` exists. No view declaration, no compatibility checker, no runtime catalog round-trip.

The cost is one extra pip package per logical workflow. For most fleets that's two to four packages total — your own — and they're tiny.

## What the master sees

The master loads every installed plugin at startup via entry-points and reads each plugin's class attributes: `Config`, `Output`, `TaskPayload`, `Pools`. For each one it generates a JSON schema via Pydantic and writes the bundle to `catalog:<automation_type>` in Redis. The dashboard reads this for rendering. The `POST /tasks` endpoint reads it for write-side validation.

You can fetch the entire catalog over HTTP:

```bash
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://master/api/v1/catalog
```

```json
{
  "cf-cookie-harvester": {
    "type": "cf-cookie-harvester",
    "kind": "BatchAutomation",
    "config": {... json schema for HarvesterConfig ...},
    "queue": {"name": "cf-cookie-harvester", "payload": {... JSON schema for HarvestRequest ...}},
    "pools": {
      "cookies": {
        "payload": {... JSON schema for CfCookie ...},
        "tags":    {... JSON schema for CfCookieTags ...}
      }
    }
  },
  "cf-scraper": {
    ...
    "stream": {"name": "cf-scraper", "payload": {... JSON schema for ScrapeResult ...}}
  }
}
```

This is informational for consumers — they don't need it to type their code, because they import the same Pydantic class the producer published.

## Validation

The framework validates payloads at the boundary, not in the middle of your slot.

`ctx.emit(payload)` checks that `payload` is an instance of the automation's declared `Output` and raises `TypeError` otherwise.

`ctx.pool(POOL).put(payload=..., tags=...)` checks that the arguments are instances of the pool's declared payload and tag models.

`ctx.submit(QUEUE, payload)` checks that the payload is an instance of the queue's declared model and serializes it for transmission.

`POST /api/v1/automations/<type>/tasks` on the master revalidates the JSON payload against the receiver's `TaskPayload` schema before queueing. If a consumer is on an older `cf-contracts` version than the producer, this is where the mismatch fails — with a clear Pydantic error pointing at the bad field.

## When the contract changes

There's no schema-evolution magic in Fleet. The producer changes its model, the consumer is on an older version, what happens depends on the change:

| Producer change              | Consumer behavior                                                               |
| ---------------------------- | ------------------------------------------------------------------------------- |
| Adds an optional field       | Old consumer ignores it. Fine.                                                  |
| Adds a required field        | Old consumer's `submit` payloads are missing it. Master rejects with 422. Loud. |
| Removes a field              | Old consumer that reads it gets `AttributeError`. Loud and immediate.           |
| Renames a field              | Old consumer's reads break the same way.                                        |
| Narrows a type (\`str        | None`→`str\`)                                                                   |
| Widens a type (`str` → \`str | None\`)                                                                         |

The cheap practice is to bump `cf-contracts` whenever the schema changes, deploy producer and consumer together when the bump is breaking, and let the version pin in `pyproject.toml` catch mismatches at install time.

## A contract package isn't always needed

If your two plugins live in the same repo and ship as one deployment, you can put the contract types alongside the producer's source and have the consumer import from there. It's still independent at the Python-package level (the consumer doesn't import the producer's automation class, just its `contract.py`), but you skip the third pip-installable artifact.

The dedicated package shape is the one to reach for when:

* The plugins are versioned and released independently.
* They might be developed by different teams.
* You want CI to catch contract drift between plugin pull requests.

Otherwise, a `contract.py` module on the producer side is fine.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://fleet.hackmap.win/concepts/contracts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
