# Organizations (v1)

> This is the recorded decision for initiative 86aec8: who may use Chatticus, what they share, and what they are allowed to approve or see.

Canonical: https://chattic.us/wiki/organizations/

This is the recorded decision for initiative 86aec8: who may use Chatticus,
what they share, and what they are allowed to approve or see.

An **organization** is an organizational unit in the multi-tenant system:
the isolation boundary that owns a computer, and the thing that has many
users. `tenant_id` is its identifier in code, keys, and the worker
protocol. "Organization" is the product name for the same object. One
identifier, two audiences. Renaming `tenant_id` was considered and
rejected: it is a partition key prefix on every item, a field on eleven
dataclasses, a column in most feature files, and part of a worker
protocol that does not redeploy in lockstep with the control plane.

A **deployment** is one AWS account running one set of stacks, and it
holds many organizations. AWS spend and the vendor project are metered
per deployment, so per-organization cost is internal chargeback rather
than an invoice. See [Budgets](/wiki/budgets). Business organizations and
AWS Organizations are separate things that may map onto Chatticus
organizations in whatever way a deployment calls for; nothing in this
design depends on that mapping.

The household case is not special. A household is an organization with
one member.

## The office model

Chatticus organizations work the way an office works. This is the
governing metaphor, and it decides the arguments below.

An office does not protect the company by keeping employees out of the
filing room. The room is shared on purpose. What differs between people
is what they may **commit the company to**, what they may **see**, and
who may **delegate** either. The compensating control for a shared room
is the audit trail, not a lock on every drawer.

| Office | Chatticus |
| --- | --- |
| The office | Organization (`tenant_id`) |
| Shared filing room, shared logins, the company card | The organization computer: `/workspace`, browser profile, credentials |
| A locked drawer | A named browser session or scoped resource whose use is itself an authority |
| Your signing authority | An authority ceiling over the consequential classes and their arguments |
| A standing instruction to the mailroom | `AutoReviewRule` always-allow with argument bindings |
| "Sam approves while I am out" | Delegation, clipped and expiring |
| Escalating to your director | Approval routing to the nearest sufficient ceiling |
| A meeting you were not in | A channel you are not a participant of |

## Identity

Authentication is outsourced. A Cognito user pool per environment
federates **Google** and nothing else. Google and Apple are native social
providers and bill against the 10,000 MAU free tier; generic OIDC
federation bills against a 50 MAU tier, which is what a GitHub shim would
have cost on top of writing the shim. Apple is deferred until an iOS
client justifies the Developer Program.

Cognito authenticates. It does **not** model organizations. Cognito
groups are the wrong home for membership: they are global to the pool,
they land in the token, and token claims go stale against the records
that govern spending. The token says who you are. Membership, role, and
ceiling are resolved server side, per request, from DynamoDB.

`user_id` is global and minted at first sign-in, keyed by verified email.
One human is one account. Their data is organization-partitioned, which
the existing keys already express.

## Enablement and hosting

Whether a signed-in person may **create** an organization is a **deployment
switch**, not a global product law. Each deployment sets
`CHATTICUS_SIGNUP_MODE`:

| Deployment | Signup mode | Product behavior |
| --- | --- | --- |
| Anthus `development` | `open` | `POST /organizations` creates an enabled, Anthus-managed org |
| Anthus `staging` and `production` | `invitation_only` until the owner opts in to public signup | Sign-in only; creation is refused until an invitation exists |
| Customer deployment | `invitation_only` | Sign-in only; creation is refused until an invitation exists |

The value is chosen per environment by `signupModeForEnvironment` in
`infra/lib/environments.ts`. To open public signup on an environment, change
that function and deploy the control plane and the web app for it.

The web SPA reads the matching build-time flag
`NEXT_PUBLIC_CHATTICUS_SIGNUP_MODE`. The HTTP front door enforces the server
value even when the web flag is wrong.

On an open-signup deployment (today only `development`), anyone may sign in and create one organization.
Creating it enables it at once and homes it in the deployment account as
Anthus-managed (`aws_setup_path` `anthus-managed`, `aws_account_id` the
deployment account): its bots and its computer run where the deployment runs,
and nothing asks for an AWS account. The first screen after creation is the
workspace.

Connecting a customer-owned AWS account is a separate, optional path and never
a precondition for using Chatticus. It is not in the product onboarding. The
API remains: the owner of a `pending` organization runs the published
cross-account CloudFormation template, then submits the AWS account id and
RoleArn at `POST /orgs/{tenant_id}/self-setup/cross-account-role`; a live
inspector validates ExternalId and required permissions, and acceptance enables
the organization with that AWS home. A `pending` organization is one an operator
created with the members CLI without enabling it. Operator `enable` (and the
members CLI `enable`, and `reinstate`) on an organization with no AWS home and
no chosen setup path makes it Anthus-managed the same way creation does. An
organization that already chose a setup path is never changed. Enabling is per
**organization**, not per person: the owner then invites their own people.

The alternative was enabling individuals, which makes Chatticus the
gatekeeper for every employee of every customer forever, and builds the
organization boundary twice because billing needs it anyway.

An enabled organization can run one organization computer, which is recurring
spend in the deployment account. On an open-signup deployment the gates on that
spend are the one-organization-per-person cap, the creation rate limit and the
monthly AWS spend ceiling an operator or owner sets; an operator can suspend an
organization. A deployment that wants a waitlist again runs `invitation_only`.

A waitlisted principal costs one DynamoDB read per request. Routing is
**default deny**: a route serves an enabled member unless it is
explicitly marked reachable by a waitlisted one. A denylist of expensive
routes was rejected, because the next route someone adds is open by
default, and one day that route is the one that calls the model.

Domain-based auto-join is excluded from v1. Joining an organization
because an email domain matches requires proving the organization owns
that domain, or someone claims a public mail domain and harvests every
signup. Invitations only.

## The shared workplace

**Decided: the computer is organization-wide.** One workplace per
organization, shared by every member and every bot.

The reason is collaboration. A shared file system is what lets a bot
continue from files another bot saved, lets a person pick up what a bot
left, and lets a bot finish what a person started. Handoff between
teammates is the product. A workplace partitioned per person or per bot
would have to reinvent handoff as a transfer.

This widens the scope in [Product](/wiki/product); it does not reverse its
principle. That document already says files under `/workspace` are
visible to every bot and that one bot continues from files another bot
saved. A per-bot workplace would have broken that. An organization-wide
one keeps it and moves the boundary from the user to the organization.

[Threat model](/wiki/threat-model) said the user is the security boundary.
For a household those were the same sentence. Now that they differ, the
**organization** is the boundary: it holds the credentials, pays the
bills, and signs the contracts.

State the consequence rather than the rule. A shared workplace means
isolation is no longer doing any of the work between members. The
authority ceiling is therefore not a later refinement; it is the only
control standing between a member and the company card, and it is
required in v1.

## Authority

Every member holds a ceiling over the consequential classes in
[Approval](/wiki/approval) and over their arguments.

> No one can grant, approve, or always-allow beyond their own ceiling.

Every task grant, approval, and always-allow rule is clipped to the
ceiling of the human who created it. A bot's effective authority for a
turn is the intersection of what the bot was granted and the ceiling of
the member who asked. Delegation is the same clip applied downward, with
an expiry; the table's TTL attribute already expires items, so a
covering-while-I-am-out delegation needs no new machinery.

A ceiling is not a subset of the five consequential classes. Real standing
is narrower and domain-shaped: one member may approve a publication to
production, another only a copy edit, another only the ingestion of a
reference. Those differ by **argument**, not by action type. Approving a
copy edit and approving a production publication are both `publish`; what
separates them is the target.

So a ceiling has the shape the kernel already uses.

| Field | Bounds |
| --- | --- |
| `action_types` | Consequential classes, each with argument constraints |
| `origins` | What may be ingested, and from where |
| `recipients` | Who may be sent to |
| `file_scopes` | What may be read or written |
| `egress_classes` | Which kinds of outbound data |
| `spend_limit` | The ceiling on `purchase` |

That is `TaskCapabilityGrant` with argument-bound action types and a spend
limit. The unification is the point: **a ceiling is a grant a person holds
standing, rather than one a task receives.** One matcher, four lifetimes.

| Object | Lifetime | Held by |
| --- | --- | --- |
| Ceiling | Standing, revocable | A member |
| Delegation | Bounded, expiring | A member, clipped from another's ceiling |
| Always-allow rule | Standing, argument-bound | The organization, authored within a ceiling |
| Task grant | One turn | A task, clipped by the granting member |
| Approval | One immutable operation | One action, bound by the approver |

Clipping is then set intersection on the same fields, everywhere.

**Roles are named ceilings, not a permission matrix.** `owner` is the
full ceiling. `member` is one that excludes `purchase` and
`production_change`. Later granularity is a change to ceiling data, not a
redesign, which defers a decision that cannot be made well yet.

Three gaps in the current kernel. The five consequential classes are an
**egress** taxonomy, inherited from a household where the only question
was what leaves. Ingesting a reference is an approval-worthy act in an
organization, and it is also the primary attack in
[Threat model](/wiki/threat-model). `TaskCapabilityGrant.origins` and
`EgressClass.APPROVED_ORIGIN_FETCH` already carry half of it; ingest needs
to become a class a ceiling can bound, not only a grant field.

The other two gaps are narrower. `AutoReviewRule.created_by` records a
**kind** (`human` or `bot`), and `rule.user_id` is a scope match, not the
author. So "a bot cannot write a rule" is enforceable today and "this
member cannot write a rule broader than their own standing" is not. The
rule needs a creator identity distinct from its scope, and approvals need
an approver identity.

Approval routing improves on the hardest problem in
[Approval](/wiki/approval), which records that v1 has no completable path
while nobody is at a screen. Presence in an organization is a team
property. An action that exceeds the requester's ceiling escalates to the
nearest member whose ceiling covers it, and `human_takeover` and
`immutable_approval` can be satisfied by any of them.

## Views

The organization has one computer and many **views** of it. A view is the
projection of the shared workplace for one member and one turn: which file
scopes, which named browser sessions, which channels.

A view is enforced where authority is enforced, at the capability sink,
not on the disk. `TaskCapabilityGrant` already carries `file_scopes`,
`origins`, and `recipients`, and a grant is clipped by the ceiling of the
member who made it. So the same mechanism that decides whether a bot may
spend money decides what it may read. The web app renders the same
projection the sink enforces; the view a person sees and the view their
bot gets are one object, not two.

`PolicyBrowserContext.storage_partition` and `Computer.browser_sessions`
already name isolated contexts that never share cookies or storage. Access
to a named session is an authority held under the same ceiling rule. That
is where per-purpose credential separation lives, and it is why an
organization-wide computer does not corner us.

Channel participation is the other half of a view, and
`ChannelParticipant{kind, actor_id}` already models it.

Say plainly what this is. A view is a professional boundary, not a vault.
It is enforced by Chatticus at the sink layer, on the operations a model
asks for. It is not an operating system permission, and a turn that
reaches a general shell on the computer is outside it. That is the same
posture as an office, where the filing room is governed by what people are
supposed to do rather than by a lock on every drawer, and it is only
defensible because the organization, not the member, is the boundary.

## Connections between organizations

Organizations inside one deployment will need to work together, so they
need a way to reach each other. This is the feature that punches a hole
in the only hard boundary the design has, so the rules matter more than
the mechanism.

`{tenant_id}#channel#{id}` is a physical partition today. A connection is
a deliberate, narrow exception to it, and it is built from the primitive
that already exists: a **clip**.

Four rules.

- **Grants name a resource, never an organization.** "Support may read this
  channel" is a connection. "Support may reach Legal" is not a
  connection, it is a merge.
- **A connection is clipped twice.** By the ceiling of the member who
  grants it, and again by what the granting tenant's owner permits to
  leave the tenant at all. Neither alone is sufficient: an individual
  should not be able to export the department, and a department policy
  should not grant what the individual granting it does not hold.
- **Received authority is borrowed, not owned.** A grant received from
  another organization is never part of the receiver's ceiling, which is what
  makes connections **non-transitive**: B cannot re-share A's grant with
  C, because B never held it. Without this distinction, re-sharing
  happens by accident and the boundary leaks one hop at a time.
- **Never at the workplace level.** A connection may reach a channel or a
  named resource. It never reaches the computer. The tenant's workplace,
  its files, and its browser sessions are the thing the boundary exists
  to protect, and a shared workplace across tenants would collapse the
  boundary entirely rather than open a door in it.

Revocable and audited follow from the above: a connection is a record
like any grant, it expires like any delegation, and every use of it is
attributable to the member who acted.

**Cost note.** One computer per organization means a deployment's Fargate
spend scales with the organizations inside it. That is the right shape,
since an organization that does no work costs nothing, but it belongs in
the deployment's budget expectations.

## Non-requirements (v1)

State these so a customer knows what they are buying.

- Chatticus does not isolate members of one organization from each
  other's credentials on the shared computer. The organization is the
  boundary.
- Bots are not a security boundary. That was already true and remains so.
- No per-member `/workspace` partition and no operating system
  permissions. Views are enforced at the capability sink, not the disk.
- No domain-verified auto-join.
- No Apple sign-in.
- No billing mechanics. The boundary billing needs is built here; the
  charging is not.

## Still open

- **Bot memory carries across channels.** `Bot.memory` is per bot, not
  per channel, and a bot reads the compacted view of every channel it
  joins. A bot in a restricted channel and an open one is a path between
  them. An office handles this with discretion; a bot has no discretion.
  Either bot memory becomes channel-scoped, or a bot's channel membership
  is itself bounded by a ceiling.
- Whether `approval_fatigue`, currently in `V1_POLICY_EXCLUSIONS`, can
  stay excluded once five people share one bot.
- Whether an organization may hold more than one computer, and if so
  whether the second one is a named resource rather than a second
  workplace.
- How audit is surfaced. The turn record already attributes every action
  to a requesting actor; nothing presents it to an owner yet.

## Documents this supersedes

These say "user" where they will say "organization" once the work lands.
They are accurate for the household today and should change with the
epic that changes the behavior, not before.

- `docs/PRODUCT.md`: the computer is isolated to the user.
- `docs/THREAT_MODEL.md`: bots are not a security boundary, the user is.
- `AGENTS.md`: the computer is shared by all of a user's bots.
