# Scope Target / Access Contract

> Canonical path: `specs/message/scope-target-access-contract.md`
> Related specs: [`scope-model.md`](./scope-model.md), [`scope-implementation-design.md`](./scope-implementation-design.md), [`reaction.md`](./reaction.md), [`rewind.md`](./rewind.md), [`search.md`](./search.md), [`../service-account/service-account.md`](../service-account/service-account.md), [`../app/app-notification.md`](../app/app-notification.md)

This spec defines the durable target/access vocabulary shared by message-domain resolvers. It keeps product-facing targets such as channel, DM, reply thread, task discussion, and service-account conversation distinct from the scope roles used by storage, delivery, read, and authorization paths.

## 1. Problem statement

AgentTeam has converged the message facts around `scope` rows, but several adapters still carry older names or local resolver shapes. When a resolver returns only one ID, callers can accidentally reuse a storage target as an authorization basis, treat a parent scope as the write target, or collapse durable author, routing principal, and notification subject into one member.

The target/access contract makes those roles explicit:

- **requested target**: what the user, agent, service, provider, or API named;
- **storage scope**: where durable facts are written;
- **delivery scope**: which scope receives fanout, realtime, wake, unread, or service-event delivery effects;
- **read scope**: which scope a history/search/attachment/read operation loads from;
- **parent scope**: parent channel/DM used by reply-thread policy;
- **permission basis**: the membership, grant, parent visibility, or service authority that authorized the operation;
- **authority subject**: the durable author, routing principal, notification subject, service principal, or execution owner considered by the resolver.

## 2. Required contract shape

A resolver that crosses a message-domain access boundary should return a typed target equivalent to the Go foundation in `internal/store/access/scope_target_contract.go`:

```go
type ResolvedScopeTarget struct {
    WorkspaceID string
    Operation   ScopeTargetOperation
    Requested   ScopeTargetRef

    Subjects []ScopeAuthoritySubject

    StorageScope  ScopeRef
    DeliveryScope ScopeRef
    ReadScope     ScopeRef
    ParentScope   ScopeRef

    RootMessageID    string
    RootMessageShort string

    PermissionBasis []ScopePermissionBasis
}
```

Not every operation uses every scope role. For example, attachment upload needs `StorageScope` and permission basis; service-event send needs `DeliveryScope`; history/read paths need `ReadScope`. The resolver must still populate each role it expects downstream callers to use. Callers must not infer an absent role from a different populated role.

## 3. Scope roles

### 3.1 Storage scope

`StorageScope` is the scope that owns durable facts such as `scope_messages`, message attachments, reaction state, search projection keys, rewind deletion boundaries, or task-scope discussion attachments.

Rules:

- A reply thread upload or message stores in the reply-thread scope, not in the parent channel/DM.
- A task discussion stores in its task discussion scope even when task board channel access authorizes the operation.
- A parent/root scope never silently replaces `StorageScope`; if parent authority is used, it is recorded as a permission basis.

### 3.2 Delivery scope

`DeliveryScope` is the scope whose fanout, notification, realtime, unread, or service-event delivery semantics are affected.

Rules:

- Ordinary visible messages generally use the same scope for storage and delivery.
- A Workflow Agent-edit request stores and delivers in the Plan's canonical
  Workflow Scope. The Workflow product resolver obtains that Scope from
  `workflow_plans.scope_id`; Browser and Agent callers do not submit an
  arbitrary Scope ID as authority.
- Service-event and task-target delivery may have a task or provider target ref while delivering into a channel/DM/thread scope; that distinction must be explicit.
- Silent/no-outbox exceptions must be expressed as operation policy, not by omitting target roles.

### 3.3 Read scope

`ReadScope` is the scope a reader, history endpoint, search query, attachment download, root-context prepender, or diagnostic preview reads from.

Rules:

- Message history/search/read-check, attachment/source/root-context checks, and diagnostic previews use the resolved read scope plus the shared SQL readable predicate / visible-row mapper; they must not re-resolve a different implicit target or re-implement public/private/owner/grant row authority later in the call stack.
- Task-scoped messages use the canonical Task->board Channel read authority: the shared readable-row predicate derives the task's unique Channel from the active Task/board authority (`tasks.board_id -> task_boards`, never the denormalized `tasks.channel_id` alone, with a `tasks.channel_id == task_boards.channel_id` mismatch guard that fail-closes) and requires the viewer's active membership on that Channel scope (matching the visible-scope authorizer and `access.ResolveActiveTaskBoardChannel`). Direct Task scope membership is state only (watch/notification/unread/read-cursor/participant projection) and never a read-authorization fact, so a retained Task membership with no Channel membership grants nothing and fails closed, non-enumerating.
- For private agents, read permission still depends on the private visibility model in `scope-model.md`; target resolution only names the scope roles and authority basis.
- Workflow transcript reads are product reads rather than generic
  conversational reads. Human reads require current Plan authoring authority
  plus the Workflow product permission basis. Agent generic history, search,
  read-check, and source-context expansion reject Workflow Scopes even when
  membership exists.

### 3.4 Parent scope and root message

`ParentScope`, `RootMessageID`, and `RootMessageShort` exist for reply-thread policy. They can authorize entry, upload, context, or notification decisions, but they do not become the child thread's storage scope.

Rules:

- If a resolver names a root message, it must also name the parent scope that contains that root.
- Parent membership, parent DM participation, parent owner-private membership, or root-message visibility can be a permission basis.
- Parent authority does not automatically create child membership or child read visibility.

## 4. Authority subjects

Resolvers must identify which subject was considered and why. A single operation may have multiple subjects:

| Subject kind | Meaning |
| --- | --- |
| `durable_author` | Member/principal that owns the durable message, attachment claim, reaction, or audit fact. |
| `routing_principal` | Principal/member used for routing, target resolution, or registered delegated-action checks. |
| `notify_principal` | Subject whose notification, wake, or realtime consequences are considered. |
| `service_principal` | Installed service/app principal and installation that is allowed to invoke the operation. |
| `caused_by_actor` | Human or domain actor whose durable decision caused the operation. |
| `execution_actor` | Authenticated Human or trusted system executor that performs the immediate operation. |
| `execution_owner` | Human owner or task execution owner whose private authority/collaborator grant is used. |

Durable author is not interchangeable with routing principal or notification subject. In particular, attachment claim ownership follows the durable author, while wake/routing may consult a different subject.

## 5. Permission basis

A permission basis records the fact that authorized the operation. Common basis kinds include:

- `direct_membership`: actor is a member of the target scope;
- `parent_membership`: parent channel/thread membership authorizes a child-scope operation;
- `parent_dm_participant`: parent DM participation authorizes a child-scope operation;
- `owner_private_membership`: owner-private channel/thread membership authorizes private-agent entry or upload;
- `task_board_channel`: task board channel access authorizes task discussion behavior; for an Agent the server resolves the unique channel from the active Task/board authority and verifies the same-workspace active Agent+Machine+Member currently holds active membership on that channel scope — client-supplied channel/taskScope is only a location/consistency hint, never the authorization fact;
- `task_collaborator_grant`: task collaborator/execution-owner grant authorizes task-scoped behavior;
- `root_message_visibility`: visible root/source message authorizes context or attachment access;
- `workflow_plan_authoring`: an active workspace Human's current Plan
  authoring authority permits Workflow transcript reads and Human edit
  requests when combined with the Workflow product target;
- `workflow_current_request`: a trusted current Agent delivery identifies the
  exact Workflow request message, Plan Scope, and selected responder allowed
  to write exactly one ordinary visible current-target text reply produced by
  that delivery. Transport chunks may compose that one reply but may not append
  multiple visible `scope_messages`; the basis never grants history access or
  authority over another Human request;
- `service_grant`: a service-installation grant authorizes the named capability, including provider behavior and installation-level service-account message.write; the target binding constrains its target scope;
- `service_account_manager`: an active Human manager assignment on an active `management_kind=workspace` account is one indivisible basis for service-account profile and manager-list administration plus service-account-authored message contribution, edit, withdrawal, attachment action, shared Pin, and top-notice actions;
- `platform_service_account_delivery`: an accepted platform broadcast plus a current platform-account workspace binding authorizes the trusted delivery service to materialize one ordinary service-account message in that workspace;
- `app_notification_recipient`: an active App installation grant, target binding,
  recipient policy, and current typed recipient fact authorize a proactive App
  notification create or new-target mutation;
- `app_notification_owner`: the App installation/member that owns an existing
  notification, together with its stored recipient policy/fact snapshots, current
  target ownership, and business-object mutation authority, authorizes update,
  resolve, or withdraw after the recipient fact reaches a terminal or deactivated
  state;
- `app_notification_action`: current message read authority, card revision, action actor policy, and the App installation action subscription authorize one Human card-action delivery;
- `archive_recovery`: archive/rewind recovery path authorizes a replay or diagnostic read;
- `system`: explicitly trusted internal/system operation.

The permission basis may name a different scope than storage, delivery, or read. That difference is part of the contract and must be preserved in reviews.

### 5.1 Workspace service-account direct-send target mapping

A Human message command whose requested `scopeId` resolves to a workspace-owned
`scopes.type=service_account` enters the service-account resolver and resolves:

- requested target: `scope:{message_scope_id}`;
- identity target: `service-account:{handle}`;
- storage, delivery, and read scope: the account's `message_scope_id`;
- durable author: the service-account principal and author member;
- service principal: the matching service-account principal, named installation, and service member;
- routing principal: the service-account principal and local author member;
- notify and message-idempotency subject: the local service-account author member;
- caused-by actor and execution actor: the authenticated Human manager;
- write permission bases: `service_grant` and `service_account_manager`;
- optional byline actor: the authenticated Human manager selected by the server from the current authorization context;
- contribution source: `service_direct_post` with the message id as its native ref.

The service account supplies the principal component of durable author and
routing principal; the service member supplies the visible author-member,
notify-principal, and idempotency-subject components. The Human manager is the
authorization, causality, execution, Audit, and optional byline actor.

Service-account Human audiences materialize as ordinary `scope_memberships` on
the account's message scope. Current direct membership authorizes history,
archive, search, permalink, source, attachment, and reaction reads and carries
Inbox participation, cursor, notification, and personal conversation state.
Workspace profile discovery uses workspace access and the service-account
profile resolver. Audience membership is fixed to every active Workspace Human;
manager assignments affect management and publishing authority but never alter
the account message scope's recipient membership.

### 5.2 Platform service-account broadcast target mapping

An accepted platform broadcast is a central control-plane command. Each
workspace delivery resolves:

- the platform service-account principal and immutable broadcast snapshot;
- a versioned platform-service-account/workspace instance binding whose
  broadcast interval contains the broadcast sequence;
- the workspace-local account, named installation, service member, grant,
  target binding, and message scope;
- durable author: the platform service-account principal plus the local author
  member;
- routing principal: the platform service-account principal plus the local
  author member;
- caused-by actor: the authenticated platform operator Human principal;
- execution actor: the trusted platform broadcast delivery service;
- notify and message-idempotency subject: the local service author member;
- write permission bases: `service_grant` and
  `platform_service_account_delivery`;
- attachment materialization/claim basis: the same platform delivery plus the
  local service-account durable author;
- contribution source: `service_direct_post` with the local message id as its
  native ref.

The delivery uses deterministic
`client_message_id=platform-broadcast:{broadcast_id}`. Each target workspace
allocates its own message id, scope sequence, attachment claims, search document,
outbox, audit facts, and runtime attention effects. The broadcast and target
ledgers retain the platform operator, payload digest, workspace target, local
message reference, attempts, and typed outcome.

### 5.3 App proactive notification target mapping

An App Notification Intent resolves one of two target forms.

For `human_dm:{member_id}` it resolves:

- requested target: the typed Human DM recipient;
- storage/delivery/read scope: the canonical App-service-member/Human-member DM;
- durable author, routing principal, notify principal, and idempotency subject:
  the installed App service principal/member;
- execution actor: authenticated App runtime or trusted business service;
- create/new-target permission bases: service grant,
  `app_notification_recipient`, and the typed
  recipient basis;
- existing-notification mutation bases: service grant and
  `app_notification_owner`, using the notification's stored recipient
  policy/fact snapshots plus current target ownership and business-object
  mutation authority;
- target binding: the canonical installation binding for that DM.

For `scope:{scope_id}` it resolves:

- storage/delivery/read scope: the named bound scope;
- the same App author/routing/notify/idempotency identity;
- active App membership, service grant, target binding, and target contribution
  policy as create/new-target permission bases;
- existing-notification update, resolve, and withdraw additionally use
  `app_notification_owner`, current target ownership, and business-object
  mutation authority.

Ordinary notifications enter the MessagePlan directly with
`service_direct_post` provenance. `app_interactive_card` adds the message-owned
card sidecar. A Human action resolves the message read scope, active notification
lifecycle, card revision, action actor policy, and action subscription before
creating the `agentteam.app.notification.action` service-event delivery.

## 6. Naming boundary

Use `scope`, `scopeID`, and named roles such as `StorageScope`, `DeliveryScope`, `ReadScope`, and `PermissionBasis` for new internal code.

Space→scope is a semantic legacy boundary, not just an id-field rename. Legacy
or compatibility names such as `spaceID`, `SpaceID`, `spaceId`, `SpaceKind`,
`SourceSpace*`, `DeliverySpaceID`, `PermissionCheckSpaceID`,
`SpaceProjection*`, `SpaceParticipant*`, `space_projection.go`, `space_*`
helpers, resolver shims, double-read fallbacks, fixtures, error text, and
current-doc mental models follow a delete-first rule: remove or rename them
whenever there is no hard current schema, history, route, workspace, or external
API constraint. Runtime code, current schema, current DTOs, and current frontend
types should use `scopeID`, `scopeId`, `targetRef`, or role-specific names.

Allowed exceptions must list a reason, boundary, and exit condition. Historical
migrations may mention old input/down-rollback names; true `workspace*` product
concepts are not scope debt; true `channelId` route/read response fields remain
channel-shaped; guardrails/spec history may name old tables only to prevent
regressions; and any external hard constraint must translate to typed scope
roles before business logic. Do not use any of these exceptions as permission to
add a new `space*` wire alias, helper, fallback, or semantic wrapper.

Do not keep a wire alias or fallback chain merely for daemon compatibility after upgraded clients have moved to `scopeId`; the agent resolve-channel response is scope-only.

Agent daemon target resolver wrappers are scope-named. `internal/store/app/agent_messages.go`, schedule/service-event owner ports, and testkit adapters must use `ResolveWritableAgentTargetScopeID`, `ResolveReadableAgentTargetScopeID`, or `ResolveAgentReviewSubjectScopeID`; do not preserve `ResolveChannelIDForAgentTarget`, `ResolveReadableChannelIDForAgentTarget`, `ResolveReviewSubjectIDForAgentTarget`, or comments claiming a scope id is exposed as legacy `spaceId`. True channel-id helper names may exist only in lower-level channel/read surfaces that actually return app channel ids, not in the agent scope resolver boundary.

Daemon receive/history/search query parameters are current scope surfaces. Scoped
receive/history/search cursors must use `scope_id`; `space_id` is not accepted as
a query alias for `scope_id`. Focused negative tests may construct retired
request keys dynamically behind helpers, but test names, helper names, comments,
and live inventories must not present retired keys as current fields. Search and
history may still accept `channel` only as the user-facing target reference to be
resolved at the boundary; it must not act as a scope-id fallback.

Attachment upload is not a legacy compatibility boundary: browser
`/api/v1/workspaces/:workspaceId/attachments` and Agent Runtime
`/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/upload`
multipart requests must use `scopeId` only, and attachment storage must receive
an explicit upload scope rather than a `spaceId` alias or post-target fallback.
Focused negative tests may construct retired request keys dynamically, but the
live upload path and ordinary test-helper naming must stay scope-named.

Browser/public message creation is not a legacy compatibility boundary:
`POST /api/messages` and `POST /api/messages/batch` request bodies must name the
target as `scopeId` only. Do not accept `spaceId` or `channelId` as create
request aliases after upgraded web and API clients have moved to scope
terminology; channel route parameters remain valid only on routes whose path is
explicitly channel-scoped. Direct API and daemon e2e callers must also send
`scopeId` so test coverage does not preserve a retired create-message key.
When that scope has `type=service_account`, the public create path must use the
service-account resolver, require a workspace-owned account and the Human
publisher basis, and set the service-account durable author, service/routing
principal, notify principal, and message-idempotency subject. The authenticated
Human remains the permission, caused-by, execution, Audit, and optional byline
actor. Platform-owned
instances receive creates from the registered platform broadcast delivery
branch described above; the branch resolves its durable broadcast and workspace
binding rather than a public author-mode input.

Task parity message-send ports use scope/target naming. Task announcement
request bodies and web types use `targetScopeId`, not `targetChannelId`.
Metadata-send ports that accept a concrete scope use `scopeID`; ports that
accept a human-readable target string use `targetRef`. Existing channel route,
notification, or review decision bridge fields may keep `channelId` only when
they truly describe a channel route.

Task collaborator access helpers use scope naming. Helpers that authorize task
collaborators against channel or thread membership must name the checked target
as a scope, for example `ensureMemberCanReadScope(scopeID)`. True workspace,
channel, and thread route identifiers may keep their product names, but helper
or local variable names must not reintroduce `ensureMemberInSpace` or generic
`spaceID`/`SpaceID`/`spaceId` aliases.

Message history/search/read models use scope naming internally. Records and
queries should expose `ScopeID`, `ScopeKind`, `ParentScopeID`, `scopeID`,
`parent_scope_id`, and `scope_id` when describing scope facts. `channel_id`
remains valid for route/read response surfaces, but internal read-model scans
must not reintroduce `SpaceID`, `SpaceKind`, `ParentSpaceID`,
`parent_space_id`, or generic `space_id` aliases for scope facts.

Daemon receive/history/search scoped cursor queries are `scope_id`-only.
`space_id` is not a read compatibility alias, and `channel` remains only a
human target reference on history/search paths rather than a scope-id fallback.

Task collaborator People grants do not need a separate legacy edge column. Current runtime, bootstrap schema, admin verification, and specs use `task_scope_participant_metadata.owner_scope_execution_id` as the grant edge; the old `participant_space_id` terminology is allowed only in historical migrations, old bootstrap fixtures, and explicit 00146 rollback/down SQL.

Service-event target bindings are also a current scope surface: runtime/domain
objects, API/DTO/web payloads, current bootstrap/admin schema, and resolver names
must use delivery/permission-check scope roles. Versioned migrations may still
mention old provider-watch column names only as upgrade input or rollback output.

Private-agent/app projection helpers, owner quote source fields, thread
subscription constants, Share/Forward owner_quote target checks, and the
ordinary private-agent-visible conversation writer are current scope surfaces:
touched paths must use `scope_projection.go`, `conversation_write.go`,
`ScopeProjection*`, `ScopeParticipant*`, `SourceScope*`, and
`scope_message_sources` rather than `space_projection.go`, `space_write.go`,
`Space*`, `SourceSpace*`, or `spaceId` aliases. Retired owner-private
context-delivery compatibility APIs must not be reintroduced as scope-target
aliases. A separate privateagent-local conversation writer may remain only while
it is the owned boundary for durable author/routing principal separation,
owner-private routing, freshness/idempotent replay, attachment claim echoing,
reply-thread participant/subscription mutation, active-task target projection,
and scope fanout outbox construction; its exit condition is migration to an
equivalent shared message-domain write service.

Agent send parameters are also a current internal contract:
`message.SendMessageParams` uses `TargetRef` for human-visible channel or
reply-thread target references, `DMTo` for direct DM sends, and `TaskScopeID`
for raw task-scope sends. It must not retain `SpaceRef` or a generic `Channel`
fallback for the same target reference. Callers resolve
`dto.AgentSendMessageRequest.Target` once at the boundary and pass only the
role-specific field downstream.

Agent channel/reply-thread target resolution is roster-based rather than an
owner-private recovery chain. Write, read, and review-subject resolvers must
resolve channel and channel reply-thread targets through a single target-basis
resolver that accepts active public or private channel roster rows. Channel
reply-thread write/review target access inherits the parent channel roster; the
root message is structural context, not an owner-private read-visibility gate.
Reply-thread topology must be loaded through a shape-only helper and authorized
by the relevant member/message/roster authority at the callsite; do not merge
thread shape and a public-only access check into one reusable helper.
Read history/search/read-check and message-derived resource surfaces still apply
the shared row-level SQL readable predicate after the target basis is resolved;
the Go visible-row layer may map/dedupe/page those SQL-visible rows, but it must
not own a second public/private/owner-grant authorization implementation. Target
access therefore does not make every historical row visible.

Private-agent target/send integration tests use scope naming for durable
storage and parent-authority assertions: helper parameters and locals must say
`wantScopeID`, `parentScopeID`, or `scopeID`, not `wantSpaceID`,
`parentSpaceID`, or generic `spaceID`. These tests may still use true channel
IDs and workspace IDs when those values model user-facing targets or tenants.

`guardrails/message/scope-target-access-debt-inventory.md` classifies the remaining legacy naming boundaries. The inventory is a ratchet: when a compatibility surface disappears, remove it from the inventory; do not use it as permission to add a new generic scope id or fallback chain.

New shared contracts and domain resolvers must not introduce fresh `SpaceID` fields for scope semantics. If an operation needs more than one scope role, add an explicit role field instead of creating another generic ID.

## 7. Resolver rules

Resolvers should follow these rules:

1. Resolve the requested target once at the boundary and pass typed roles downward.
2. Return the authority subject and permission basis that allowed the operation.
3. Keep parent/root scope as authorization context only unless the operation explicitly stores, delivers, or reads from that parent scope.
4. Do not use `ErrForbidden` / `ErrNotFound` fallback chains as the primary architecture for target semantics. Compatibility recovery paths must be named, bounded by the requested target kind, listed in the target/access debt inventory, and normalized into a typed decision before downstream callers use the result. Compatibility adapters may translate final decisions to existing errors after the typed decision is known.
5. Keep resolver cost bounded by the requested target, candidate target list, or explicit page/job scope; do not enumerate unrelated workspace data to infer authority.
6. Tests should lock both the allowed role mapping and the denied/unauthorized case when a resolver distinguishes child storage from parent authority.
7. Product Scope types such as `workflow` require an explicit resolver branch.
   Unknown or product-only Scope types must not fall through to generic
   membership authorization, conversation search, unread, or Channel defaults.

## 8. Guardrail

`guardrails/message/scope-target-access-contract.sh` protects the typed contract
surface, checks that the durable target/access debt inventory exists, and prints
remaining semantic `space`→`scope` legacy matches in target/access-adjacent
packages. The inventory is a migration aid and ratchet; it is not a license to
add new generic space naming, old mental models, or unnamed resolver fallback
chains.
