# Scoped source auth and plugin-owned overrides

Working model for shared sources with personal/workspace/org auth.

## Problem

Sources can be shared at an outer scope while auth often belongs at an
inner scope.

Examples:

- An organization adds an OpenAPI source once, but every member brings
  their own API token or OAuth client credentials.
- A workspace shares one connection by default, but an individual user
  overrides it with their own connection.
- A future policy says an org only allows low-risk operations to auto-run,
  while a user or workspace has a narrower or broader typed policy.

The common primitive is not "org auth" or "user auth". It is an ordered
scope stack:

```ts
[user, workspace, org];
```

Rows still belong to exactly one scope. Resolution walks the stack
innermost first, with outer scopes acting as shared defaults.

## Boundary

Core owns generic primitives:

- Scope stack and scope ids.
- Source registry metadata: id, owning scope, plugin id, display fields.
- Secrets and connections.
- Tool invocation enforcement once a plugin reports annotations/policy.

Plugins own source-domain meaning:

- How a source authenticates.
- What auth slots exist.
- How slots are used during invocation.
- Per-source/per-scope config patches.
- Per-source/per-scope policy rules.

Do not put plugin-specific config into a generic core JSON bag. Core
should not know that an OpenAPI source uses an Authorization header, that
an OAuth flow is client credentials, or that `GET` operations are safer
than `POST` operations. Those are plugin concerns.

## OpenAPI model

OpenAPI has a typed base source row:

```ts
openapi_source {
  id
  scope_id
  spec
  base_url
  headers // static strings or typed slot references
  oauth2  // typed OAuth template with slot ids
}
```

The source row describes the template, not a globally connected account.
For example:

```ts
oauth2: {
  flow: ("clientCredentials", tokenUrl, clientIdSlot, clientSecretSlot, connectionSlot, scopes);
}
```

Scoped auth material lives in OpenAPI-owned rows:

```ts
openapi_source_binding {
  source_id
  source_scope_id
  target_scope_id
  slot
  value: { kind: "secret", secretId }
       | { kind: "connection", connectionId }
       | { kind: "text", text }
}
```

The storage column is intentionally `target_scope_id`, not `scope_id`.
This keeps the table out of the SDK scoped-adapter convention, because a
source owner must be able to clean up all bindings for that source even
when some bindings target descendant user scopes. OpenAPI still filters
and validates visibility manually against the active scope stack.

OpenAPI resolves bindings across the current scope stack. The plugin then
uses its typed source config to decide how the resolved material is
applied. For example, a slot may become an HTTP header with a prefix, or
an OAuth connection may provide a bearer token.

Core is only involved when resolving the underlying primitive:

```ts
const binding = await openapiStore.resolveSourceBinding(source, slot);

if (binding.value.kind === "secret") {
  const value = await ctx.secrets.get(binding.value.secretId);
}

if (binding.value.kind === "connection") {
  const token = await ctx.connections.accessToken(binding.value.connectionId);
}
```

## Add-source UX

Adding a source and adding auth should be separable.

Expected flow:

1. Admin adds a source at an outer scope.
2. Admin chooses the auth template the source supports.
3. Source can be saved without entering secret values or completing OAuth.
4. Users or workspace admins later fill the auth slots at the scope they
   want to own.
5. Invocation resolves the innermost matching binding.

This avoids making the first person who adds a source accidentally
provide auth for everyone.

## OAuth flows

Authorization code and client credentials use the same slot model.

Authorization code:

```ts
source oauth template:
  clientIdSlot
  clientSecretSlot?
  connectionSlot

user bindings:
  clientIdSlot -> shared or personal client id secret
  clientSecretSlot -> shared or personal client secret secret
  connectionSlot -> user's Connection
```

Client credentials:

```ts
source oauth template:
  clientIdSlot
  clientSecretSlot
  connectionSlot

user bindings:
  clientIdSlot -> user's client id secret
  clientSecretSlot -> user's client secret secret
  connectionSlot -> user's app-style Connection
```

`Connection.kind` remains a core concept because it describes the
connection/token identity. The pointer from a source auth slot to a
connection is plugin-owned scoped data.

## MCP direction

MCP should follow the same shape when it needs shared source plus
personal auth:

- MCP source row owns the typed auth template.
- MCP plugin owns scoped auth binding/config rows.
- Core connections/secrets remain the underlying primitives.
- Invocation resolves through the plugin-owned scoped rows.

Do not make MCP depend on OpenAPI's binding table. The shared abstraction
is the scope stack and the helper patterns, not a cross-plugin table that
contains plugin semantics.

## Policy direction

Auto-run/approval policy should not be stored as generic source override
JSON.

Core should enforce the final invocation decision, but plugins should own
typed policy rules when the rule language is domain-specific. For
OpenAPI, a policy may mention HTTP methods or operation ids. For MCP, it
may mention MCP tool metadata. Those meanings are not core concepts.

Shape:

```ts
plugin scoped config/policy rows:
  source_id
  source_scope_id
  target_scope_id // or another plugin-owned scope field, not core magic
  typed plugin policy fields

plugin.resolveAnnotations(tool, scopeStack):
  derive ToolAnnotations from plugin-owned rows

core invoke:
  enforce ToolAnnotations
```

## What not to do

- Do not add a generic `source_override.value` JSON blob in core for
  plugin config.
- Do not store effective plugin config twice, once in plugin tables and
  once in core.
- Do not make slot names meaningful to core.
- Do not bake user/org/workspace concepts into SDK storage. Scope ids are
  flat; the host builds the ordered stack per request.
