Tenant workspace resources
Tenant workspace resources for Q integrations.
Q1.1 replaces public filesystem roots with opaque, tenant-owned workspace IDs. The server provisions storage and chooses execution policy. An ID alone grants no access. This is a breaking security change to workspace requests, including the legacy /api/v1 answer alias. Ordinary answers without workspace access do not need a workspace ID.
Provision and revoke
Apply the Alembic migration through the normal operator release process. The new q_service.workspace_resources table has forced row-level security, independent of Q_ENABLE_RLS. Run the application with a non-superuser database role without BYPASSRLS; grant that role the required table privileges through the deployment's normal database-role provisioning. The registry binds q.tenant_id for each transaction and also filters queries by tenant. Do not expose database credentials to API callers.
Set WORKSPACE_STORAGE_ROOT to a pre-created, absolute, canonical directory owned by the Q service account. No component in this path may be a symlink. For example, use /srv/q/workspaces on Linux; /tmp on macOS is a symlink, so use its canonical location when testing. Workers and API processes must use the same storage and registry. Keep the base directory inaccessible to workspace callers and container processes; mount only the selected UUID directory for execution.
These are operator commands, using the operator's configured application environment:
python -m scripts.workspaces create --tenant example-tenant --label "Project files"
# Opt in to execution only for resources intended to run code:
python -m scripts.workspaces create --tenant example-tenant --label "Build workspace" --allow-execute
python -m scripts.workspaces revoke --tenant example-tenant --workspace-id WORKSPACE_UUIDCreation returns workspace_id, label and execution_enabled; it never accepts or returns a host path. Revocation disables new lookups while retaining files. Each tool operation checks the registry again. Revocation does not cancel a command already running: cancel its run or wait for the bounded command timeout. File deletion, retention and storage quotas need separate operator policy.
Credentials and capabilities
Issue the smallest required scopes. The developer key issuance API accepts these scopes:
| Scope | Permits |
|---|---|
workspace:read | Discover owned resources; read/list files, modes, plans and artefacts; attach workspace context to an answer/run |
workspace:write | Write/edit/delete files; save or delete plans |
workspace:execute | Run supported shell/Python commands and verifier commands in an execution-enabled resource |
workspace:* | All three capabilities |
Answer and agent operations retain their existing AI scope requirements. Use ai:answer plus the necessary workspace scopes for those routes. workspace:execute also permits file changes through code, so treat it as a powerful permission. ai:*, first_party and disabling AI-scope enforcement do not grant workspace access. The administrative * scope does. First-party credentials must carry explicit workspace scopes too.
Ownership is at tenant level; this task does not add per-user or per-key workspace ACLs within one tenant. On resume/approval, Q reauthorises the resource using the current caller's scopes and persists the resulting grant. It never silently inherits the original caller's more powerful grant. Old runs without a resource grant return 409; start a new resource-bound run.
API examples
Send the usual authentication header. Discover available IDs with GET /v1/workspaces. Responses contain at most 100 enabled resources.
PUT /v1/workspaces/WORKSPACE_UUID/files/notes/readme.txt
Content-Type: application/json
{"content":"Synthetic project notes"}GET /v1/workspaces/WORKSPACE_UUID/files/notes/readme.txt
GET /v1/workspaces/WORKSPACE_UUID/files?q=readme&limit=30
GET /v1/workspaces/WORKSPACE_UUID/artifacts/notes/readme.txtFile responses contain workspace_id, relative path and content. Artefacts return bytes as an attachment. Each read/write is limited to 1,000,000 bytes. Text reads replace invalid UTF-8; use the artefact route to preserve bytes. Directory scans are bounded and omit .git, .venv, node_modules and __pycache__. There is no unbounded recursive listing.
POST /v1/answer
Content-Type: application/json
{
"query": "Summarise these project notes",
"mode": "ask",
"workspace_id": "WORKSPACE_UUID",
"explicit_context": [{"type":"file", "reference":"notes/readme.txt"}]
}Plan and mode routes use the same resource:
GET /v1/plans?workspace_id=WORKSPACE_UUID
GET /v1/plans/PLAN_ID?workspace_id=WORKSPACE_UUID
DELETE /v1/plans/PLAN_ID?workspace_id=WORKSPACE_UUID
GET /v1/custom-modes?workspace_id=WORKSPACE_UUID
GET /v1/workspace/files?workspace_id=WORKSPACE_UUID&q=readmeUse save_plan: true with plan mode and write permission, or load_plan_id with read permission. Plans live in .q/plans within the resource. Custom modes in .q/modes retain prompt/read-only/tool restrictions, but cannot grant automatic execution approval or install host hooks. File, folder, plan and codebase explicit context use the resource; codebase matching here is bounded text matching, not a semantic index.
Create a standard agent with config.workspace_id or supply workspace_id to POST /v1/agents/{agent_id}/runs. The latter overrides the configured resource for that run after ownership/scope checks. Public requests and workers carry a server-issued resource grant through nested tools and delegated runs. Checkpoint state operations and dashboard resumes reauthorise the resource. They do not restore host directories.
Unknown, disabled and other tenants' workspace IDs all return 404. Missing scopes return 403. Absolute paths, traversal, Windows drive paths, links and special files are rejected. Raw root/sandbox overrides, including nested values, return 422 with workspace_paths_and_sandbox_overrides_forbidden_use_workspace_id.
Supported execution
Set WORKSPACE_SANDBOX_IMAGE to an operator-reviewed, locally available image@sha256:<64 hex characters> reference. The image must provide sh for shell execution and python3 for Python execution. The Alpine test fixture proves shell isolation only; it is not a Python runtime recommendation. The Docker daemon must have access to the same canonical storage path. Prepare images separately; requests use --pull=never.
Execution requires the resource's allow_execute permission and the caller's workspace:execute scope. Supported tools are shell_exec and Python code_exec. Container policy is fixed: network disabled, read-only image, only the resource mounted writable at /workspace, no forwarded application environment/credentials, all capabilities dropped, no new privileges, 64 processes, 512 MiB memory, one CPU, and a 64 MiB temporary directory. Commands run as the Q process UID/GID. Use a non-root service account in production.
Timeouts are clamped to 1–120 seconds, output is bounded to 1,000,000 bytes per stream, and cancellation/timeout removes the container. No configured image, an unavailable daemon/image or failed isolation returns an unavailable/error result. There is no host-process fallback. Containers share the host kernel: use a separately isolated execution fleet if your deployment requires stronger protection. Total workspace disk use and tenant-wide concurrency quotas are separate operational controls.
Supported resource file tools are file_read, find_files, grep_search, file_write, file_edit and file_delete. grep_search uses bounded literal text matching. Legacy browser/computer/Git/project-index and other host action tools return tool_unavailable_for_workspace_resources until they have equivalent resource adapters.
Deployment migration and unavailable legacy features
Before deploying:
- Apply the registry migration, configure storage, and provision the required tenant resources.
- Issue appropriate workspace scopes to API and first-party credentials.
- Copy only reviewed project files into each resource through authorised file operations or controlled operator tooling. Q does not automatically import old host directories, credentials, hooks or shared plans.
- Replace
workspace_root,workspace_path,workspace_roots,cwdand caller sandbox settings withworkspace_idand relative paths in every client. The same prohibition applies inside agent configuration andextra_context. - Restart legacy resource-less agent runs as new runs. Enable execution only after preparing and testing the chosen image.
Existing UI/extension flows and SDK types that still submit host paths require client migration before release. This task implements the server contract and HTTP workflow; it does not claim those clients have been redesigned or generated clients regenerated. Python keyword options and TypeScript extensible answer options can pass workspace_id; direct HTTP remains available. Full hosted SDK/frontend qualification is still required.
Public cloud-agent creation/execution/handoff, agent cloud handoff/reattach, best-of-N worktree execution/merge and the legacy multitask endpoint return 503 while their backends lack resource adapters. Workspace Git/terminal explicit context and project hooks are unavailable. Basic answer/agent runs, supported files/plans/modes, DB checkpoint state and resource artefact downloads remain available. The legacy delivery-pack endpoint can render stored manifest information but will not read a stored host report path; use the resource artefact route for downloads.
This deliberate security break has no opt-out to restore arbitrary host access. The rationale and follow-on work are in ADR 0003. External-agent registration, shared outbound destination policy and broader permissions remain separate programme tasks.
Verification
Run the deterministic setup, then the required unit, contract, integration, migration and type checks in the release guide. make test-services now prepares the pinned Alpine fixture as well as PostgreSQL, Redis and Temporal. Integration tests verify the real registry policy with a restricted PostgreSQL role, container access boundaries and timeout cleanup. No live provider credentials are required. See Q1.1 evidence for measured results and release limitations.