A resource pool is a finite set you allocate from - a range of ports on one machine today. It is the first thing makermap hands out rather than merely records, and everything about how it behaves follows from one fact:
makermap does not touch your infrastructure. It cannot connect to a host, cannot scan a port, and has no way to know that something is already listening on 3000.
So an allocation here is a record of an agreement, not an occupied socket. Read that sentence twice before wiring this into a deploy: it is the difference between a tool that helps and a tool you would be right not to trust.
1. Define the pool
Settings → Infrastructure → Resource matrices. A pool has a kind (port), a name, optional binding to a host, and its ranges:
{ "ranges": [{ "from": 8000, "to": 8099 }] }
Bind it to a host and collisions are counted per machine, which is how ports actually work. Leave it unbound and they are counted per organization, which is how subdomains and database names will work when those kinds are registered.
Defining a pool is a human act: there is no MCP tool for it, and the REST routes refuse an agent principal. Deciding which ports a machine may use is a judgement about a machine makermap cannot see. Taking the next free one out of that decision is not, and that is exactly where the line falls.
2. Take a value
allocate_resource pool="net: app ports" label="tidepool api"
You get the lowest free value. Ask for a specific one with preferred and it is either yours or refused - never silently substituted, because a port you did not choose is worse than a refusal you can act on. Ask for several with count; they are the lowest free ones and are not promised to be consecutive.
Two agents asking at the same moment never receive the same value. That is a database-level guarantee, not a hope: the write takes an advisory lock on the pool and a partial unique index makes two live holders of one value impossible.
Pass an idempotencyKey if a retry is plausible. Without one, a connection that drops after the write leaks a port on every retry - and the leak is invisible, because both allocations look valid.
3. Give it back
release_resource pool="net: app ports" value="8003" note="service retired"
The row is stamped, never deleted, so "who held 8081 in March" stays answerable. Nothing expires on its own and there is no TTL: an expiring hold would silently free a value a live application is still listening on, and the next caller would be handed it as free. A value nobody uses any more is visible in the grid and costs one call to fix. The asymmetry is deliberate.
4. Report what you actually see
The pool has to be able to hold what makermap never handed out, or the first allocation on a real machine is wrong.
record_resource_observation pool="net: app ports" value="3000" observed="taken" label="old nginx"
Reporting a value as taken records it, attributed to you, marked as a report rather than an allocation. Reporting one as free does not release it. A report is evidence, not authority: releasing on it would hand the value to the next caller while something nobody told us about is still listening. The disagreement shows up in get_resource_pool and a person settles it.
What the refusals mean
| You see | What happened |
|---|
POOL_EXHAUSTED | Everything is held. The message names the capacity, how many are held and how many you asked for. |
VALUE_ALREADY_HELD | Somebody has the value you asked for. The holder is named when the record knows it. |
VALUE_NOT_IN_POOL | Outside the ranges, malformed, or - on release - not currently held. |
POOL_KIND_UNKNOWN | The pool declares a kind this server does not register. Retrying cannot help. |
Nothing is taken and nothing already held is disturbed by any of them.
Scopes
resources:read to look, resources:write to allocate, release and report. They are separate from infra:read / infra:write on purpose: taking a port should not require the right to rewrite the server registry.