Tags
Ownership in Introspection is per-member: a file owned by one end user is invisible to every other. That is right for personal data and wrong for shared working material — a team’s documents, a customer-wide knowledge base, a memory file several agents maintain together.
A share answers “let this one member read this one resource”. It does not answer “everyone working on Acme can reach Acme’s files”, because there is no group: you would mint one share per member per resource and re-mint on every new member and every new file. Tags answer that.
The whole rule
A caller may read and write a task or file when the caller’s own tags and the row’s tags intersect.
member.tags ∩ resource.tags ≠ ∅ → the caller can reach the rowThat is the entire mechanism. No tag key is reserved, none is interpreted, and nothing is derived from a tag’s text. A tag means exactly what your organization decides it means, and two tags are related only by being equal.
It is inert until tags exist on both sides: a caller with no tags matches nothing, and a row with no tags is matched by nobody.
A tag used for filtering becomes an ACL the moment the same string appears
on a member. If you tag files env:prod to filter them and also tag a member
env:prod to filter members, that member gains read and write over those
files. Nothing in the system distinguishes an “accounting” tag from an “access”
tag, because adding that distinction would mean interpreting a tag’s text.
Keep the two vocabularies apart by convention, and audit with the reads in
Who can reach what.
Two sides, two different gates
Which side of the match you are writing decides how privileged the write is, and this asymmetry is the security model:
| You are writing | Who may write it | What it does |
|---|---|---|
| A task or file’s tags | The row’s owner, or a privileged caller | Pushes that row toward a cohort |
| A member’s tags | An organization owner (members:manage) | Pulls every row carrying the tag toward that member |
Only the privileged side can pull. Tagging your own file can hand it to a cohort you are not in — but that is you giving away your own resource, which a share already let you do. Writing a tag onto a principal is what has to be restricted, because that is the shape a grant takes.
A tag-shared collaborator can update a row’s content, title, metadata, and
lifecycle state, and can perform the ordinary recoverable delete. What they
cannot do is change the row’s tags: a patch that supplies tags matches no row
unless they own it. So a collaborator admitted by a tag cannot remove the tag
that admitted them, and cannot re-share the resource to a different cohort.
Deletes are inside the shared set because both resources are recoverable — files are versioned, so deleting the head reverts to the prior version, and tasks are soft-deleted.
Shares are a separate mechanism and are untouched by any of this. A share stays a read-only grant on one resource.
Writing tags
| Where | How | Available in |
|---|---|---|
| Task, at creation | tags on task create | SDKs, CLI (--tag), REST |
| Task, later | tags on task update | SDKs, REST |
| File | tags on file update | SDKs, REST |
| Member | tags on member update, organization owner only | Dashboard, REST |
| A member an identity assertion is about to mint | identity.tags when opening a runner | SDKs, CLI (--identity-tag), REST |
Tagging a member is the one write with no SDK surface: it is an organization-owner operation, done from the dashboard or with a REST call. That asymmetry is deliberate — it is the privileged half of the model in Two sides, two different gates.
The CLI sets tags when it creates a task, but does not filter a list by one; reach for an SDK or REST for that.
A tag list replaces wholesale. Omit tags to leave the existing list
untouched; pass [] to clear it. This is unlike a metadata patch, which merges
— so an explicit empty list has to reach the wire, and a client that drops falsy
values from a request body will silently fail to clear a tag.
CLI
introspection tasks create --runtime support-agent --environment production \
--prompt "Draft the Q3 summary." --tag customer:acme --tag team:researchTags on a file follow the file, not the version
A file’s versions are separate rows and a list returns the head, so a new version inherits both the tags and the owner of the version before it. Without that, a tag would silently stop matching at v2 — the new head was never tagged, so the intersection would be empty and every tag-shared collaborator would lose access with no error. Writing a new version records who wrote it separately from who owns it, so a tag-shared writer never quietly takes ownership of a file.
What a tag may contain
A tag is an opaque string. key:value is the convention every example here
uses, and the one to follow — but it is only a convention: a bare acme is an
equally valid tag, because set membership does not care.
| Rule | Value |
|---|---|
| Maximum length | 128 characters |
| Maximum tags per resource | 64 |
| Must be | Non-empty, free of whitespace and control characters |
| Matching | Exact and case-sensitive — Acme and acme are two different tags |
| Duplicates | Collapsed; the remaining order is preserved |
Invalid tags are rejected, not trimmed. Silently turning "acme " into
"acme" would store a tag you never sent, which under a set match is an access
grant you never asked for. Nothing is normalized or lower-cased for the same
reason: normalizing would be one more thing reading a tag’s text.
Filtering
tag is an optional filter on the task, file, and member list endpoints:
for await (const task of runner.tasks.list({ tag: "customer:acme" })) {
console.log(task.id, task.tags);
}
for await (const file of runner.files.list({ tag: "customer:acme" })) {
console.log(file.name, file.tags);
}The filter only narrows. It is combined with the access rule rather than replacing it, so naming a tag you do not carry returns nothing rather than granting you anything.
Who can reach what
Two reads answer the audit question from either end:
GET /v1/members?tag=team:acme— which members can reach rows carrying it.GET /v1/tasks?tag=team:acmeandGET /v1/files?tag=team:acme— which rows.
Nothing joins them, and there is no group registry to enumerate: members live in the Control Plane while tasks and files live in the Data Plane, which is also why the caller’s tags have to travel in the session token rather than being looked up.
Revocation is eventual. Because the caller’s tags ride the session token, removing a member’s tag takes effect at the next token mint, not immediately — the same behavior as changing a member’s role.
Tagging a customer as they arrive
When your backend opens a runner and asserts an end-user identity, identity.tags
stamps the customer member that assertion mints. That puts your end user into
a cohort at the moment they first appear, instead of asking an organization owner
to tag them afterwards.
CLI
introspection tasks create --runtime support-agent --environment production \
--subject user_123 --identity-tag customer:acme --prompt "Draft the Q3 summary."--identity-tag requires --subject: tags stamp the member an identity mints,
so with no identity asserted there is no member to stamp.
Two bounds make this safe, and both are enforced by the server:
- Attenuated. The asserted list is narrowed to the tags the asserting agent member already holds; anything else is dropped silently. An assertion can only hand out what an organization owner already granted, so a tag still originates in exactly one place. Excess tags are dropped rather than raising, since the list is a hint about a customer and failing a whole session over one ungrantable tag would turn a typo into an outage.
- Applied on create only. Tags are stamped when the member row is first inserted and never on a later assertion. A returning customer cannot be re-tagged, stripped, or have an owner’s grant clobbered by a repeat call. Changing an existing member’s tags stays an owner operation.
This grants nothing until an organization owner deliberately tags the agent member: an agent member with no tags of its own can assert none.
Choosing between tags and shares
| Need | Use |
|---|---|
| Let one named person read one file or conversation | A share |
| Let a changing group read and write a growing set of tasks and files | A tag on the rows and on each member of the group |
| Group work for filtering and reporting only | A tag — but pick a vocabulary no member tag will ever use |
| Let someone reproduce work from a conversation | A share, then a fork |
Related
- State & artifacts: files, shares, forks, and owner-scoped memory.
- Tasks & runs: the executions tags are set on.
- Authentication & identity: how a caller establishes the identity a tag is stamped on.
- Organizations & projects: members, roles, and the owner role that writes member tags.