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:
- Owned by the requesting workspace — always allowed
- Granted to the requesting workspace — allowed if a valid
workspace_agent_grantsrow exists and has not expired - Global agent (no
workspace_idin DB, e.g. defined inastra.yml) — allowed - Owned by another workspace with no grant —
403 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
| Field | Type | Description |
|---|---|---|
receivingWorkspaceId | string | The workspace that will receive access |
agentId | string | The agent being shared |
readonly | boolean (default true) | When true, the receiving workspace can chat with the agent but cannot spawn sub-agents from it |
expiresAt | ISO 8601 datetime, optional | Grant 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.