Summary
Make secret connections (workspace_secret_connection) optionally user-scoped, so a personal API key isn't forced into a workspace-shared bucket.
The inconsistency
TAS already has two credential models, scoped differently:
- Native-MCP connections (
workspace_connection) — per-user (user_id); each member OAuths their own.
- Secret connections (
workspace_secret_connection, keyed by (workspace_id, slug)) — workspace-shared. No user_id; only a created_by audit column.
So a personal API key (e.g. a user's attio_api_key) stored as a secret connection is shared: any member's agent in the workspace can use it, and anyone with secret access can rotate/overwrite it. (The plaintext is never exposed — AES-encrypted, UI shows only …last4 — so the exposure is usability + existence, not the raw value.)
Proposal
Add an optional owner scope to secret connections:
- Schema: add nullable
user_id to workspace_secret_connection; a row is workspace-shared when null, personal when set. Key/uniqueness becomes (workspace_id, COALESCE(user_id,'workspace'), slug) or similar.
- Creation UI: a visibility toggle — "Me" / "Workspace" (mirrors the webhook "Run as" / connection ownership patterns).
- Listing/visibility: a member sees workspace-shared secrets + only their own personal ones.
- Resolution: when a run dispatches, resolve the run owner's personal secret first, then fall back to the workspace-shared one.
tas_tools.secret(name) keeps the same call shape; the resolution becomes owner-aware.
Code anchors
api/migrations/0039_workspace_secret_connection.sql — schema (add user_id).
web/src/lib/secret-connections.ts — CRUD + listing (scope the queries, add owner).
- The run's secret bundle (
TAS_SECRETS) is assembled per-workspace today (web/src/lib/... dispatch → runner); it would need to be assembled for the run owner (workspace-shared ∪ owner's personal).
- The Secret connections settings UI (add the visibility toggle).
- AAD:
aadSecretConnection(workspaceId, slug) (web/src/lib/crypto-aad.ts) would need the owner folded in for personal rows.
Scope note / when it matters
For providers that offer OAuth (Attio, etc.), the cleaner answer is to use the per-user native-MCP connection instead of an API-key secret at all — that's already per-user. This feature is for API-key-only providers where a per-user credential has no other home today.
Surfaced by: the attio_api_key secret being workspace-shared while the Attio OAuth connection is per-user (single-tenant dogfood).
Summary
Make secret connections (
workspace_secret_connection) optionally user-scoped, so a personal API key isn't forced into a workspace-shared bucket.The inconsistency
TAS already has two credential models, scoped differently:
workspace_connection) — per-user (user_id); each member OAuths their own.workspace_secret_connection, keyed by(workspace_id, slug)) — workspace-shared. Nouser_id; only acreated_byaudit column.So a personal API key (e.g. a user's
attio_api_key) stored as a secret connection is shared: any member's agent in the workspace can use it, and anyone with secret access can rotate/overwrite it. (The plaintext is never exposed — AES-encrypted, UI shows only…last4— so the exposure is usability + existence, not the raw value.)Proposal
Add an optional owner scope to secret connections:
user_idtoworkspace_secret_connection; a row is workspace-shared when null, personal when set. Key/uniqueness becomes(workspace_id, COALESCE(user_id,'workspace'), slug)or similar.tas_tools.secret(name)keeps the same call shape; the resolution becomes owner-aware.Code anchors
api/migrations/0039_workspace_secret_connection.sql— schema (adduser_id).web/src/lib/secret-connections.ts— CRUD + listing (scope the queries, add owner).TAS_SECRETS) is assembled per-workspace today (web/src/lib/...dispatch → runner); it would need to be assembled for the run owner (workspace-shared ∪ owner's personal).aadSecretConnection(workspaceId, slug)(web/src/lib/crypto-aad.ts) would need the owner folded in for personal rows.Scope note / when it matters
For providers that offer OAuth (Attio, etc.), the cleaner answer is to use the per-user native-MCP connection instead of an API-key secret at all — that's already per-user. This feature is for API-key-only providers where a per-user credential has no other home today.
Surfaced by: the
attio_api_keysecret being workspace-shared while the Attio OAuth connection is per-user (single-tenant dogfood).