SPEC-0025: Artifact Assignment
Graph Edges
- Implements: ADR-0032: a native, reassignable assignee on artifacts.
- Requires: SPEC-0002: the artifact aggregate the assignee attaches to, and the tag rules that keep tags routing hints.
- Requires: SPEC-0012: the outbound envelope, signing and delivery the new event rides on.
- Requires: SPEC-0023:
authorizeRead, the owner model, teams and roles the assignment authorization reuses. - Related: SPEC-0007 (the MCP surface gaining
artifact_assign), SPEC-0016 (the lifecycle-event registry the new kind joins).
Overview
An artifact MAY carry one assignee: a nullable reference to a user, set at
creation and changeable or clearable afterwards by the users who may already
change the artifact's metadata. The assignee is announced by two signed events:
assignee inside artifact.created's data, and a new artifact.assigned
kind on every effective change. Tags remain client-asserted routing hints
(ADR-0018); the assignee is the validated, reassignable identity field.
Requirement IDs (AS-n) are stable. Plans and code cite them as
SPEC-0025 AS-n.
Requirements
Requirement: AS-1 Assignee Model and Invariants
An artifact MUST carry at most one assignee, stored as an artifact assigned to user V while visibility is the owner tightens visibility to V is assigned to an artifact V does not own V gains no ability to delete, rotate, re-share or expiry-change it
beyond what an assignment replaces a previous assignee the artifact has exactly one assignee_user_id on
the artifact: nullable, a foreign key to users, never an owner column. The
assignee MUST NOT change the artifact's owner, visibility, expiry or access
policy; it is a named worker, not a co-owner. An assignee MUST be a user who
passes authorizeRead for the artifact at the moment of assignment. A stale
assignment — one whose user can no longer read the artifact after a visibility
or membership change — MUST NOT be retroactively cleared: read authorization
continues to gate reads (SPEC-0023), and the assignment's event remains the
record of the intent at the moment it was made.Scenario: Assignment survives a visibility tightening
linkprivateassignee_user_id is unchanged, and V's reads now fail
authorizeRead exactly as any other non-reader'sScenario: Assignee is not an owner
authorizeRead already grantsScenario: One assignee
assignee_user_id afterwards and the
previous one survives only in the emitted event
Requirement: AS-2 Who May Assign
A set, change or clear MUST be refused unless the actor may change the artifact's metadata:
- a personal artifact: the owning user, including an agent acting for them;
- a team artifact: any current, unsuspended member of the owning team.
Assignment is metadata, not sharing policy: an agent MAY assign within its
principal's rights, where member B of team T calls the assignee is replaced and one a signed-in user who is not the owner calls the response is an agent authorized by the owner calls the assignment succeeds; the event's sharing:manage (visibility, SPEC-0023 REQ
"Visibility Values") stays closed to agents. A refused request MUST answer
403 and change nothing.Scenario: Member reassigns a team handoff
artifact_assign on a T artifactartifact.assigned event is emittedScenario: Non-member cannot assign
artifact_assign on a
private artifact403 and the assignee is unchangedScenario: Agent assigns for its principal
artifact_assign on the
owner's link artifactactor_id and actor_kind
record the principal and agent
Requirement: AS-3 Assignee Must Be a Reader
The assignee named in a create or assignment request MUST resolve to an
existing user who passes a create carries the request fails with the owner assigns user V to the owner's the request fails with the owner of a the request fails with the owner assigns any existing user to a the assignment succeeds and the user can read the artifactauthorizeRead for the artifact. An unknown username
MUST answer 400 assignee_unknown. A user who exists but cannot read the
artifact MUST answer 400 assignee_unreadable for a private artifact, and
for a team artifact a non-member MUST answer the same. For link visibility
any signed-in user qualifies. The answers MUST be identical in shape and timing
to the validation-error standards (SPEC-0002 REQ "Error Handling Standards").Scenario: Unknown username refused
assignee=nosuchuser400 assignee_unknown and nothing is persistedScenario: Private artifact, other user refused
private artifact400 assignee_unreadableScenario: Team artifact, non-member refused
team artifact assigns a user outside the team400 assignee_unreadableScenario: Link artifact, any signed-in user accepted
link artifact
Requirement: AS-4 Setting at Create
Every create surface MAY accept an assignee at creation: MCP an agent calls the artifact is created with that assignee and the returned artifact
names them a create carries no artifact row, blob or event exists afterwards a create carries no assignee the artifact is created exactly as today, with no assignee key in
the response or the eventartifact_create
and bundle_create gain optional assignee (a username); REST create accepts
assignee=<username> as a query parameter, X-Cairn-Assignee header, or
multipart field. Validation (AS-3) runs before persistence, inside the create
transaction, so a rejected create persists nothing and emits no
artifact.created. The create response MUST return the assignee's username
when set, and nothing when not.Scenario: Create with assignee
artifact_create with assignee=joestumpScenario: Rejected assignee persists nothing
assignee_unreadable-quality inputScenario: Omitted assignee changes nothing
Requirement: AS-5 Changing and Clearing
the assignee moves from U to V exactly one the assignee is cleared one the response is the assignment transaction fails after validation no event is emitted and the previous assignee is intactartifact_assign (MCP) and PUT /v1/artifacts/{id}/assignee (REST, body
{"assignee": "<username>"} or {"assignee": null}) MUST set or clear the
assignee in one transaction. An assignment to the user already assigned MUST
succeed idempotently with 200 and emit nothing. Every effective change MUST
emit exactly one artifact.assigned event after the transaction commits, from
the service layer that owns the change, through the same choke point as every
lifecycle event (SPEC-0016 REQ "EV-2 Emission Points"). Emission MUST NOT
block, fail or alter the response.Scenario: Reassign emits once
artifact.assigned event is emitted, carrying the new
and previous valuesScenario: Clear emits with a null new value
artifact.assigned event is emitted whose new assignee is
absent (null), not an empty stringScenario: Same-value write is silent
artifact_assign names the current assignee200 with the artifact unchanged and no event is
emittedScenario: Rolled-back change emits nothing
Requirement: AS-6 Event Payloads
artifact.created's data MUST gain an optional assignee — the assigned
user's display handle — subject to the additive rules of SPEC-0012 REQ "Event
Payload": omitted when unset, so an artifact without an assignee produces a
byte-identical event to today's. A new kind artifact.assigned MUST be added
to the SPEC-0016 EV-1 registry before it is emitted, and its data MUST carry:
- the subject-artifact and principal fields every event carries (EV-3);
assignee: the assignee after the change (omitted when the change cleared it), the same field the router projects;assignment: an object carryingprevious— the assignee immediately before the change, omitted when there was none.
Both kinds MUST be delivered only to the owning workspace's subscriptions
(SPEC-0023 REQ "Events Go Only to the Artifact's Workspace"), signed per
SPEC-0012 REQ "Signed Delivery", and honoured by the subscription kind filter. an artifact is created with an artifact is created without an assignee its the assignee moves from U to V the a subscription filters kinds to an Scenario: Created event carries the assignee
assignee=joestumpartifact.created's data.assignee is "joestump" and the
signature covers itScenario: Unassigned create is byte-identical
artifact.created body contains no assignee key and is
byte-identical to the pre-feature payload for the same field valuesScenario: Assigned event shape
artifact.assigned body carries data.assignee = "V",
data.assignment.previous = "U", and the standard envelope fieldsScenario: Kind filter applies
artifact.created onlyartifact.assigned change is not delivered to it
Requirement: AS-7 API and MCP Surface
REST: an agent calls the read shows the assignee, and a second a user who cannot read an artifact calls the response is PUT /v1/artifacts/{id}/assignee requires authentication, CSRF, and the
AS-2 authorization check; it MUST answer with the artifact in the same shape as
a read, including the assignee's username when set. MCP: a new artifact_assign
tool takes the artifact id and an optional assignee (omitted or null clears),
and follows the MCP tool-surface and error standards (SPEC-0007, SPEC-0019).
Both surfaces MUST answer 404 — not 403 — for an artifact the caller cannot
read, per the uniform not-found rule.Scenario: MCP round trip
artifact_assign with assignee=joestump, then
artifact_readartifact_assign with the
same value is a silent idempotent successScenario: Unreadable artifact answers 404
artifact_assign on it404, identical to an unknown id
Requirement: AS-8 UI
The artifact page MUST show the assignee when set — display handle, not email —
with who assigned and when from the event history where available. The Bin
MUST accept an a reader opens an assigned artifact the assignee's display handle is visible on the page the owner lists the Bin with only artifacts currently assigned to that user are returned, and
artifacts whose assignment was later cleared are not a user opens "assigned to me" the Bin is filtered to artifacts assigned to them that they can
still readassignee=<username> filter and an "assigned to me" view backed
by the AS-9 index, and MUST show assigned artifacts where they are shown today
plus the filter. The UI MUST NOT offer assignment to a user who would fail
AS-3, and MUST NOT offer it to actors who would fail AS-2.Scenario: Assignee shown on the artifact
Scenario: Bin filtered by assignee
assignee=joestumpScenario: Assigned to me
Requirement: AS-9 Database Shape
One migration SHALL add the migration runs on a database with existing artifacts every artifact has a null assignee and nothing else changes the Bin is listed with an assignee filter the query is index-backed on assignee_user_id BIGINT NULL REFERENCES users(id) to
artifacts and a partial index ON artifacts (assignee_user_id) WHERE assignee_user_id IS NOT NULL for the assigned-to-me path. All access MUST use
bound parameters (SPEC-0002 REQ "Database Operation Standards"), and the
assignee write MUST join the create or assignment transaction it belongs to.
The migration MUST NOT backfill or rewrite any existing row.Scenario: Migration is additive
Scenario: Index serves assigned-to-me
assignee_user_id
Requirement: AS-10 Golden Tests
The outbound golden tests MUST pin the new shapes:
- an
artifact_createdgolden with an assignee, whosedata.assigneeis present and signed; - an
artifact_assignedgolden, carryingassigneeandassignment.previous; - the existing unassigned
artifact_createdgoldens MUST remain byte-identical.
A payload change that breaks either invariant MUST fail the suite. the emitter changes and an unassigned artifact's payload differs
from its golden the test suite fails the emitter encodes the output matches Scenario: Goldens guard additivity
Scenario: Goldens pin the assigned event
artifact.assigned with previous U and new Vartifact_assigned.golden.json field for field