Workspace

Cross-Workspace Agent Grants

Agent grants let workspace A share one of its agents with workspace B. Without a grant, agents are private to the workspace that owns them — any chat request referencing an agent owned by another workspace is rejected with 403.

Resolution order

When a chat request arrives with an agentId, the gateway resolves it through the following chain:

  1. Owned by the requesting workspace — always allowed
  2. Granted to the requesting workspace — allowed if a valid workspace_agent_grants row exists and has not expired
  3. Global agent (no workspace_id in DB, e.g. defined in astra.yml) — allowed
  4. Owned by another workspace with no grant403 Forbidden

Creating a grant

Requires owner or admin role in the granting workspace.

bash
# Grant workspace B access to workspace A's "research-agent"
curl -X POST http://localhost:3000/workspaces/ws_A/grants \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "receivingWorkspaceId": "ws_B",
    "agentId": "research-agent",
    "readonly": true
  }'

Grant fields

FieldTypeDescription
receivingWorkspaceIdstringThe workspace that will receive access
agentIdstringThe agent being shared
readonlyboolean (default true)When true, the receiving workspace can chat with the agent but cannot spawn sub-agents from it
expiresAtISO 8601 datetime, optionalGrant expiry; null means permanent

Expiring grants

Set expiresAt to create a time-limited grant:

bash
# Grant access that expires after 30 days
curl -X POST http://localhost:3000/workspaces/ws_A/grants \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "receivingWorkspaceId": "ws_B",
    "agentId": "research-agent",
    "readonly": true,
    "expiresAt": "2026-03-28T00:00:00Z"
  }'

Expired grants are not automatically deleted — the resolution logic checks expires_at < NOW() and treats expired rows as non-existent. Clean them up periodically with a DELETE WHERE expires_at < NOW() query.

Revoking a grant

Send a DELETE to /workspaces/:id/grants with the same receivingWorkspaceId and agentId:

bash
# Revoke the grant
curl -X DELETE http://localhost:3000/workspaces/ws_A/grants \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "receivingWorkspaceId": "ws_B",
    "agentId": "research-agent"
  }'

Database schema

sql
workspace_agent_grants (
  id                    UUID PRIMARY KEY,
  granting_workspace_id TEXT NOT NULL,
  receiving_workspace_id TEXT NOT NULL,
  agent_id              TEXT NOT NULL,
  readonly              BOOLEAN DEFAULT true,
  granted_by            TEXT NOT NULL,
  granted_at            TIMESTAMPTZ,
  expires_at            TIMESTAMPTZ,   -- NULL = never expires
  UNIQUE (granting_workspace_id, receiving_workspace_id, agent_id)
)
Granting is idempotent — a second POST to the same (granting, receiving, agentId) triple updates the existing row's readonly and expires_at rather than inserting a duplicate.