Isolate Agents on a Shared Instance
Run several agents on one GoodMem instance, each confined to its own space, with one service identity and one ceiling-locked key per agent.
Isolate Agents on a Shared Instance
A common deployment has several agents sharing one GoodMem instance, each with its own space as its memory. The requirement is that each agent can read, search, and write its own space and nothing else — and that if an agent's credential leaks, it still cannot reach a sibling's space.
The pattern is three records per agent:
- A service identity — the agent's own principal, so its access does not hang off any person's account.
- A
SPACE_CONTRIBUTORrole assignment on the agent's space — its live authority. - A scoped API key whose ceiling mirrors the role — the credential the agent actually holds.
The ceiling is the same four rules the role grants, so the agent's live authority and its credential agree exactly:
| Operation | Selector | Why |
|---|---|---|
READ_SPACE | EXACT | Read the space's own record |
LIST_MEMORY | EXACT | First retrieval gate |
READ_MEMORY | DIRECT_MEMBERS_OF | Second retrieval gate; also reading individual memories |
CREATE_MEMORY | EXACT | Write new memories |
This is partitioning of workloads within one organization. It is not multi-tenancy; see What This Is Not.
Before You Start
- An administrative human credential. The instance
ADMINrole covers everything below; otherwise you needCREATE_SERVICE_IDENTITY,MANAGE_ACCESSon each agent's space, andCREATE_API_KEY. - One space per agent, already created. The recipe below handles one agent; step 7 loops it.
export GOODMEM_REST_URL="https://localhost:8080" # REST base URL; on GoodMem Cloud, your instance's https:// hostname
export GOODMEM_API_KEY="gm_your_admin_key"
export AGENT_NAME="agent-billing"
export AGENT_SPACE_ID="<UUID of this agent's space>"The CLI speaks gRPC, which GoodMem Cloud instances do not expose. On Cloud, use the cURL or HTTPie tab — every step is a plain REST call over HTTPS — or the console. If your own server uses a self-signed certificate, add -k to curl and --verify=no to HTTPie.
1. Create the Agent's Identity
goodmem service-identity create --display-name "$AGENT_NAME"
export AGENT_ID="<the UUID from the output>"export AGENT_ID="$(curl -sS --json "{\"displayName\": \"$AGENT_NAME\"}" \
"$GOODMEM_REST_URL/v1/service-identities" \
--header "x-api-key: $GOODMEM_API_KEY" | jq -r '.serviceIdentityId')"export AGENT_ID="$(http POST "$GOODMEM_REST_URL/v1/service-identities" \
x-api-key:"$GOODMEM_API_KEY" displayName="$AGENT_NAME" | jq -r '.serviceIdentityId')"Display names are unique for the life of the instance, tombstones included, so name agents durably (agent-billing, not agent-1).
2. Give It the Space Role
goodmem space access role assign $AGENT_SPACE_ID \
--principal $AGENT_ID \
--role SPACE_CONTRIBUTORcurl -sS --json @- "$GOODMEM_REST_URL/v1/access-policy/role-assignments" \
--header "x-api-key: $GOODMEM_API_KEY" <<JSON
{
"principalId": "$AGENT_ID",
"role": "SPACE_CONTRIBUTOR",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" }
}
JSONhttp POST "$GOODMEM_REST_URL/v1/access-policy/role-assignments" \
x-api-key:"$GOODMEM_API_KEY" \
principalId="$AGENT_ID" \
role="SPACE_CONTRIBUTOR" \
assignedResource:="{\"kind\": \"SPACE\", \"resourceId\": \"$AGENT_SPACE_ID\"}"A role is one record that reads clearly in audits and in the console's Space Access page. Four direct grants (one per rule above) are equivalent and are what the CLI's service-identity onboard --grant shortcut creates; use whichever you would rather see in the policy listing.
3. Issue the Agent's Key
Assign the role first — the server checks every ceiling rule against the subject's live authority at issuance, and the identity has none until step 2.
goodmem apikey create \
--subject $AGENT_ID \
--authority-mode scoped \
--ceiling READ_SPACE:EXACT:SPACE:$AGENT_SPACE_ID \
--ceiling LIST_MEMORY:EXACT:SPACE:$AGENT_SPACE_ID \
--ceiling READ_MEMORY:DIRECT_MEMBERS_OF:SPACE:$AGENT_SPACE_ID \
--ceiling CREATE_MEMORY:EXACT:SPACE:$AGENT_SPACE_ID \
--label agent=$AGENT_NAMEcurl -sS --json @- "$GOODMEM_REST_URL/v1/apikeys" \
--header "x-api-key: $GOODMEM_API_KEY" <<JSON
{
"subjectPrincipalId": "$AGENT_ID",
"authorityMode": "SCOPED",
"ceiling": [
{ "operation": "READ_SPACE", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "CREATE_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } }
],
"labels": { "agent": "$AGENT_NAME" }
}
JSONhttp POST "$GOODMEM_REST_URL/v1/apikeys" x-api-key:"$GOODMEM_API_KEY" <<JSON
{
"subjectPrincipalId": "$AGENT_ID",
"authorityMode": "SCOPED",
"ceiling": [
{ "operation": "READ_SPACE", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "CREATE_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } }
],
"labels": { "agent": "$AGENT_NAME" }
}
JSONThe raw key (rawApiKey, gm_...) is returned once. Put it straight into the agent's secret store; the server keeps only a verifier.
The agent authenticates every request with this key in the x-api-key header and never sees your administrative key.
4. Why These Four Rules
LIST_MEMORYis the one people forget. Semantic retrieval checks two gates for every space in the request:LIST_MEMORYwithEXACTon the space, andREAD_MEMORYwithDIRECT_MEMBERS_OFon it. A ceiling ofREAD_SPACE,READ_MEMORY, andCREATE_MEMORY— the intuitive "read and write" set — produces an agent that can write memories but getsPERMISSION_DENIEDon every search.DIRECT_MEMBERS_OFis what makes the space boundary real. It covers every memory the space contains now or later, and nothing outside it. AnANY-selector rule would cover memories in every space the agent could ever be granted.- Nothing else. No
LIST_SPACE, so the agent cannot even enumerate sibling spaces. NoDELETE_MEMORY; if an agent should prune its own memory, assignSPACE_CONTENT_MANAGERinstead and addDELETE_MEMORY:DIRECT_MEMBERS_OFto the ceiling. NoEXECUTE_EMBEDDER; retrieval and ingestion run the space's configured embedders on the space's behalf.
5. What a Leaked Key Can Do
Nothing beyond its ceiling, no matter what the identity is later granted. Every request through a scoped key must pass two checks: the subject's live authority and the ceiling. The ceiling never grows, so even if someone assigns agent-billing a role on a second space, the key issued in step 3 still cannot touch it. The identity gains authority; the credential does not.
The reverse also holds. Revoke the role assignment from step 2 and the key stops working on the next request, even though the key itself is untouched: there is no longer any live authority under the ceiling. That is the fastest way to suspend one agent without rotating anything.
6. Verify the Boundary
Using the agent's key, ask the authorization check endpoint about its own space and a sibling's:
GOODMEM_API_KEY="gm_agent_key" goodmem access-policy check \
--check LIST_MEMORY:SPACE:$AGENT_SPACE_ID \
--check LIST_MEMORY:SPACE:$OTHER_AGENT_SPACE_ID \
--check LIST_SPACE:INSTANCE \
--format quietcurl -sS --json @- "$GOODMEM_REST_URL/v1/access-policy:check" \
--header "x-api-key: gm_agent_key" <<JSON
{
"checks": [
{ "operation": "LIST_MEMORY", "target": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "LIST_MEMORY", "target": { "kind": "SPACE", "resourceId": "$OTHER_AGENT_SPACE_ID" } },
{ "operation": "LIST_SPACE", "target": { "kind": "INSTANCE" } }
]
}
JSONhttp POST "$GOODMEM_REST_URL/v1/access-policy:check" x-api-key:"gm_agent_key" <<JSON
{
"checks": [
{ "operation": "LIST_MEMORY", "target": { "kind": "SPACE", "resourceId": "$AGENT_SPACE_ID" } },
{ "operation": "LIST_MEMORY", "target": { "kind": "SPACE", "resourceId": "$OTHER_AGENT_SPACE_ID" } },
{ "operation": "LIST_SPACE", "target": { "kind": "INSTANCE" } }
]
}
JSONExpect true, false, false. Checks are advisory; a real retrieval against the sibling space with the agent's key fails with PERMISSION_DENIED (HTTP 403), and retrieval against its own space succeeds. Issue Scoped API Keys shows both calls.
7. Do It for Every Agent
The three calls are the same for every agent, so script them. The REST version builds each body with jq so UUIDs never need hand-quoting:
# name=space-uuid, one entry per agent
AGENTS=(
"agent-billing=$SPACE_BILLING"
"agent-support=$SPACE_SUPPORT"
"agent-research=$SPACE_RESEARCH"
)
for entry in "${AGENTS[@]}"; do
name="${entry%%=*}"; space="${entry#*=}"
key="$(goodmem service-identity onboard --display-name "$name" --quiet \
--grant READ_SPACE:EXACT:SPACE:$space \
--grant LIST_MEMORY:EXACT:SPACE:$space \
--grant READ_MEMORY:DIRECT_MEMBERS_OF:SPACE:$space \
--grant CREATE_MEMORY:EXACT:SPACE:$space)"
echo "$name $key" # deliver to the agent's secret store, not to a log
done# name=space-uuid, one entry per agent
AGENTS=(
"agent-billing=$SPACE_BILLING"
"agent-support=$SPACE_SUPPORT"
"agent-research=$SPACE_RESEARCH"
)
api() { curl -sS --json "$2" "$GOODMEM_REST_URL/v1/$1" --header "x-api-key: $GOODMEM_API_KEY"; }
for entry in "${AGENTS[@]}"; do
name="${entry%%=*}"; space="${entry#*=}"
id="$(api service-identities "$(jq -n --arg n "$name" '{displayName: $n}')" \
| jq -r '.serviceIdentityId')"
api access-policy/role-assignments "$(jq -n --arg id "$id" --arg s "$space" '{
principalId: $id, role: "SPACE_CONTRIBUTOR",
assignedResource: {kind: "SPACE", resourceId: $s}}')" > /dev/null
key="$(api apikeys "$(jq -n --arg id "$id" --arg s "$space" --arg n "$name" '{
subjectPrincipalId: $id, authorityMode: "SCOPED", labels: {agent: $n},
ceiling: [
{operation: "READ_SPACE", selector: "EXACT", assignedResource: {kind: "SPACE", resourceId: $s}},
{operation: "LIST_MEMORY", selector: "EXACT", assignedResource: {kind: "SPACE", resourceId: $s}},
{operation: "READ_MEMORY", selector: "DIRECT_MEMBERS_OF", assignedResource: {kind: "SPACE", resourceId: $s}},
{operation: "CREATE_MEMORY", selector: "EXACT", assignedResource: {kind: "SPACE", resourceId: $s}}
]}')" | jq -r '.rawApiKey')"
echo "$name $id $key" # deliver to the agent's secret store, not to a log
doneThe CLI loop uses service-identity onboard, which records the four rules as direct grants rather than a role; the result is the same authority. Rerunning either loop for an agent that already exists fails on the identity's unique name, which is the safe outcome — resume with the individual calls instead.
8. Rotate, Suspend, Retire
- Rotate a key: issue a second key for the same
subjectPrincipalIdwith the same ceiling, deploy it, thenDELETE /v1/apikeys/{oldKeyId}(goodmem apikey delete). The identity and its role are untouched. - Suspend an agent: revoke its role assignment (
DELETE /v1/access-policy/role-assignments/{id}, orgoodmem access-policy role-assignment revoke). Its key fails every request immediately; reassign the role to restore it. - Retire an agent: delete the service identity. Every key whose subject it is stops authenticating, and the identity becomes a permanent tombstone. If the agent might come back, revoke its keys and keep the identity instead.
What This Is Not
Each agent here is a workload belonging to one organization, partitioned by policy inside one instance. That is what grants, roles, and ceilings are for, and they do it precisely. It is not tenant isolation: a GoodMem instance is one database serving one tenant, and separating customers from each other requires separate instances. See One Tenant per Database.
See Also
- Set Up a Service Identity — the single-workload version, with rotation and ownership hand-off in depth
- Issue Scoped API Keys — the ceiling grammar and refusal cases
- Built-in Roles — exactly what
SPACE_CONTRIBUTORand the other space roles contain - API Keys and Ceilings — why the ceiling and the live authority are checked independently
- Console: Access Control — the same three records from the browser
Set Up a Service Identity
Give a production workload its own principal and scoped API keys, so credentials rotate without touching the workload's identity or access.
Upgrade to the New Authorization Model
What changes for existing deployments when the new authorization model arrives, and what to do about each change.