protocol/task/capabilities
packages/protocol/src/task/capabilities
Purpose
Public barrel for R-channel capability tag classes. Tag classes + value types, plus therefine* helpers (which validate
an already-fetched row and need no server service). The obtain*
helpers that depend on server-side services live in
@moltzap/server-core — inline in app/capability-providers.ts, with
the composites in task/services/.
Public surface
AgentExists
Class
AgentExistsValue
Interface
agentId resolves to a real, active agents row.
Value payload carries ownerUserId (nullable, since unclaimed agents
are valid existence proofs but have no owner).
AgentInTaskParticipants
Class
AgentInTaskParticipantsValue
Interface
agentId is in task_participants for taskId.
Used by TaskConversationAddParticipant (D1’s new handler) to prove
the agent being added to a conversation already participates in the
parent task — today’s inline task_participants query becomes the
capability obtain.
assertConversationInTaskMatches
Function
(taskId, conversationId) pair
equals the expected pair. Fails with ForbiddenError on the first
mismatch; runs both comparisons in one Effect for handler-side
symmetry with assertTmAuthorityMatchesTask.
assertTaskReadAccessMatchesTask
Function
cap.task.id === expectedTaskId for TaskReadAccess. The
value shape mirrors TmAuthorityValue; a separate overload keeps the
type narrowed at the call site.
assertTmAuthorityMatchesTask
Function
cap.task.id === expectedTaskId. Fails with
ForbiddenError when the handler obtained a capability for one task
but passed a different taskId argument to the service method.
ContactPolicyAllowsReach
Class
ContactPolicyAllowsReachValue
Interface
ConversationCreateAuthorization
Class
ConversationCreateAuthorizationValue
Interface
ConversationInTask
Class
ConversationInTaskValue
Interface
conversation.task_id === taskId.
assertCapabilityMatchesTask (see assert-capability-matches-task.ts)
verifies the carried taskId matches the handler-input taskId at
call time — the one-line runtime check that catches “handler passed
a different taskId than the obtain proved”.
ConversationNotArchived
Class
ConversationNotArchivedValue
Interface
conversation.archived_at IS NULL.
Refine-shape: takes the archived_at column read inline by the
caller. Folded into the composite MessageSendPermission value
for the MessagesSend path (every constructor verifies the
conversation is open).
GroupCapacityForCreate
Class
GroupCapacityForCreateValue
Interface
invitedAgentIds to a new
task respects policy limits on group capacity. Required by
TaskRequest ONLY when invitedAgentIds.length > 1.
Value payload carries (creatorAgentId, invitedAgentIds) to match
the obtain-time argument set; service methods consuming the capability
verify the count matches handler input.
MessageSendPermission
Class
MessageSendPermissionValue
Interface
MessageService.send — Architect Decision A
in plan #606.
One tag carrying one payload shape. The handler obtains the value
via provideServiceEffect; the service body destructures the
carried proof rows directly.
Earlier revisions of this surface modelled a three-arm discriminated
union (forParticipantOnActiveTask | forTmBypass | forTmBypassWithReply) so the TM could bypass the
refineTaskActive gate when sending into a failed task. The
downstream MessageService.sendInsert never discriminated the
variants, no production caller exercised the failed-task window,
and the TM gate is now proved at obtain time via app-ownership of
the calling WS connection rather than a per-variant bypass flag.
The bypass mechanism was removed in the #673 follow-up.
noReplyTarget
Function
NoReplyTarget
Class
NoReplyTargetValue
Interface
ObtainConversationCreateAuthorizationInput
Interface
ObtainMessageSendPermissionInput
Interface
MessagesSend params + the authenticated
ctx.agentId; the constructor handles the conversation lookup,
participant check, task-active refinement, reply-target check, and
returns the populated value.
refineConversationNotArchived
Function
ConversationArchivedError when
archivedAt is non-null. Consumed by obtainMessageSendPermission
after the conversation projection lookup.
refineTaskActive
Function
TaskClosedError when status is
closed / failed. Consumed by obtainMessageSendPermission on
the non-TM-bypass branch.
TaskActive
Class
TaskActiveValue
Interface
closed / failed).
Refine-shape: takes a SendConversationRow already fetched by
MessageService.readSendConversation and validates the task_status
column inline. No DB call. Consumed by the composite
MessageSendPermission.forParticipantOnActiveTask obtain helper.
The TM-bypass branch is NOT a TaskActive proof — it’s modeled in
the composite MessageSendPermission.forTmBypass constructor instead.
Staleness window
TaskActive is a liveness proof — tasks.status can transition
active → closed between obtain and use. The refine helper is safe
to call inside the same transaction that reads the task row;
cross-transaction reuse is a defect (re-obtain by re-reading the
column).
TaskReadAccess
Class
TaskReadAccessValue
Interface
task (initiator OR
admitted task_participant).
Value payload carries the task row already fetched by today’s
TaskService.loadTaskWithReadAccess check; consumers reuse the payload.
Consumed by the task.service.ts public methods (get, getMessages,
getMessagesSince) via the R-channel; handlers wire the value with
Effect.provideServiceEffect(TaskReadAccess, obtainTaskReadAccess(...)).
TmAuthority
Class
TmAuthorityValue
Interface
task.appId. The obtain helper resolves the proof
via AppHost.isAppConnection(task.appId, callerConnId); the bind
is stable across requests on the same WS and invalidated by the
connection’s Scope finalizer.
Value payload carries the task row already fetched by the obtain
helper so consumers don’t re-query.
ValidReplyTarget
Class
ValidReplyTargetValue
Interface
ValidReplyTarget / NoReplyTarget is required by
MessagesSend. The two tags model the input-shape branch:
input.replyToId !== undefined obtains ValidReplyTarget (which
verifies the referenced message exists in the target conversation);
input.replyToId === undefined obtains the zero-payload
NoReplyTarget constructor.
Per Architect Decision A, these two tags are FOLDED into the
composite MessageSendPermission value (every constructor variant
carries one of the reply-target proofs) — they’re not provided as
separate R-channel tags at the MessagesSend handler. They remain
standalone tags so D1 / future handlers can require them
independently if/when needed.
Files
agent-exists.tsagent-in-task-participants.tsassert-capability-matches-task.tscontact-policy-allows-reach.tsconversation-create-authorization.tsconversation-in-task.tsconversation-not-archived.tsgroup-capacity-for-create.tsmessage-send-permission.tsreply-target.tstask-active.tstask-read-access.tstm-authority.ts