GoodMemGoodMem
How-To GuidesUsers and Access

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:

  1. A service identity — the agent's own principal, so its access does not hang off any person's account.
  2. A SPACE_CONTRIBUTOR role assignment on the agent's space — its live authority.
  3. 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:

OperationSelectorWhy
READ_SPACEEXACTRead the space's own record
LIST_MEMORYEXACTFirst retrieval gate
READ_MEMORYDIRECT_MEMBERS_OFSecond retrieval gate; also reading individual memories
CREATE_MEMORYEXACTWrite 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 ADMIN role covers everything below; otherwise you need CREATE_SERVICE_IDENTITY, MANAGE_ACCESS on each agent's space, and CREATE_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_CONTRIBUTOR
curl -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" }
}
JSON
http 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_NAME
curl -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" }
}
JSON
http 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" }
}
JSON

The 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_MEMORY is the one people forget. Semantic retrieval checks two gates for every space in the request: LIST_MEMORY with EXACT on the space, and READ_MEMORY with DIRECT_MEMBERS_OF on it. A ceiling of READ_SPACE, READ_MEMORY, and CREATE_MEMORY — the intuitive "read and write" set — produces an agent that can write memories but gets PERMISSION_DENIED on every search.
  • DIRECT_MEMBERS_OF is what makes the space boundary real. It covers every memory the space contains now or later, and nothing outside it. An ANY-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. No DELETE_MEMORY; if an agent should prune its own memory, assign SPACE_CONTENT_MANAGER instead and add DELETE_MEMORY:DIRECT_MEMBERS_OF to the ceiling. No EXECUTE_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 quiet
curl -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" } }
  ]
}
JSON
http 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" } }
  ]
}
JSON

Expect 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
done

The 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 subjectPrincipalId with the same ceiling, deploy it, then DELETE /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}, or goodmem 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