# Sign-in, connections, roles, access levels, networks, budgets, transcripts

## Contents
- How members sign in (tenant.json auth)
- Connections
- Roles and access levels
- Directory sync
- Networks
- Budgets
- Transcripts

## How members sign in (`tenant.json` `auth`)

| `kind` | When | Fields |
|---|---|---|
| `invite` | The default. Streamliner's own sign-in (Sign in with Apple, passkey, email code); members come from invites, admins or directory sync. Owners and admins may always use it. | — |
| `oidc` | The organization has an identity provider. | `issuer`, `clientId` (a public client), `scopes` (with `openid`, `offline_access`), `resources[]` (each MCP server's resource id), `roleClaim` (default `groups`), `storeClaim` |
| `connection` | No IdP; signing in to the named system opens the session. | `connection` |
| `demo` | Demo accounts: pick a user from `users[]`. | `users[]` |

## Connections

One system a person signs in to as themselves. Every MCP server entry names one (`mcp[].connection`, default its id);
servers on one system share it, so a person signs in **once per system**, however many streamlines use it.

```json
"connections": [
  { "id": "backoffice", "name": "Meridian back office", "type": "password",
    "signIn": { "usernameLabel": "Username", "help": "Your back-office username and password." } },
  { "id": "crm", "name": "Meridian CRM", "type": "oauth", "scope": "person",
    "signIn": { "help": "Use your Meridian staff login." } },
  { "id": "layaway", "name": "Layaway accounts", "type": "oauth", "scope": "chat",
    "signIn": { "help": "The customer will get a PIN by text." } }
]
```

| `type` | Meaning |
|---|---|
| `oauth` | The MCP authorization spec (protected-resource metadata, authorization-server metadata, dynamic client registration, PKCE, RFC 8707 `resource`), **on the person's device**: the system's own page in a sheet, the token in the device's Keychain per account and connection, sent device → the MCP server. Never to Streamliner's back end, the relay or the model. |
| `password` | A native card; the back end logs in once and returns the system's session to the device. Needs `private/connections/<id>.json` (`login.url`, `login.body` with `{{username}}`/`{{password}}`, `session.from` `cookie` or `json:<path>`, `session.send.header`, `ttlSeconds`). The password is never stored, logged or shown to the model. |
| `api-key` | The person's own key for that system, on the same kind of card (designed). |
| `none` | Public, read-only tools only. |

`oauth` connections take these too (all optional):

| Field | Meaning |
|---|---|
| `scope` | `person` (default): one sign-in per person, kept until it ends or they sign out. `chat`: **each chat signs in on its own**, for a system where the session *is* the customer (customer-authorized): the chat starts with *Sign in for this customer*, the token lives and dies with that chat (completed, archived, deleted, cleared by retention, signed out), and the sheet shares no cookies with the last customer's. A `chat` connection can't be `auth.connection`. |
| `authorizationServer` | The issuer, when the MCP server publishes no protected-resource metadata. |
| `scopes` | OAuth scopes to ask for, when the server advertises none. |
| `resource` | The RFC 8707 audience (`mcp[].resource` works too; the connection's wins). |
| `signIn.help` | One line on the Connect Your Accounts page and the sign-in card. Say whose login it is. |

**When people are asked.** Switching into the organization shows **Connect Your Accounts** for every `person`
connection a streamline they can use names and this device has no token for (all present: no page; *Not now* lets them
in, and those streamlines show a small sign-in mark). In a chat, a call that answers 401 (or 403 `insufficient_scope`,
or a code listed in the server's `reauthOn`, streamlines.md) is held behind *Sign in to <connection name>* and retried
once signed in. A plain 403 is a permission refusal, never a sign-in.

configcheck: an `oauth` server's declared connection must be of type `oauth` (`connection.type`); `scope`,
`authorizationServer`, `scopes` and `resource` are for `oauth` connections only (`connection.oauthOnly`); a `chat`
connection can't be the account's sign-in (`connection.chatScope`); an `oauth` connection no server names is a warning
(`connection.unused`); `reauthOn` needs `auth: "oauth"` (`gate.reauth`).

Never write a person's password, session or token into config. Never use a shared service account: each call must
be made as the signed-in person (law 3).

## Roles and access levels

- **Roles** (`tenant.json` `roles[]`) are job roles: who may open which streamline (`agents/<kind>.json` `roles`) and
  use which tool (`tools[].roles`). OIDC accounts map claims to roles; others assign roles by invite or directory.
- **Access levels** `owner | admin | member` sit on the membership: owners and admins manage the account (invites,
  usage, transcripts); only an owner (or a server key) invites an owner. Seed them in `private/tenant.json`
  `access: { "<userId>": "admin" }`. Recommend at least two owners.

## Directory sync

The organization's system pushes users, roles and networks with a server key (scope `directory`):
`PUT | PATCH /v1/admin/<account>/directory`, body
`{ users?: { <id>: { name, roles: [..], networks?: [..], email?, username?, access?, disabled?, store?, title? } | null },
roles?: { <id>: { name?, networks?: [..] } | null }, networks?: { <id>: { name, cidrs: [..] } | null } }`.
PUT replaces everything pushed; PATCH replaces the entries named; `null` removes. Idempotent (`changed: false`).

## Networks

A streamline can be limited to the organization's networks. The rule is private; the device file only says
`"network": { "restricted": true, "message": "Approvals works only on the store or office network." }`.

```json
// private/tenant.json — named networks, who is on which, and the account default rule
{ "network": { "mode": "none" },
  "networks": { "harbor-point": { "name": "Harbor Point store", "cidrs": ["198.51.100.16/28", "2001:db8:12::/48"] },
                "head-office": { "name": "Head office", "cidrs": ["203.0.113.0/24"] } },
  "roles": { "regional": { "networks": ["head-office"] } },
  "users": { "u-marco": { "networks": ["harbor-point"] } } }

// private/agents/<kind>.json — the streamline's rule (wins over the account default)
{ "network": { "mode": "user" } }                                // inside one of the caller's own networks
{ "network": { "mode": "named", "networks": ["head-office"] } }  // inside one of these
{ "network": { "mode": "none" } }                                // unrestricted
{ "network": { "allow": ["198.51.100.0/24"] } }                  // legacy CIDR list; [] = unrestricted
```

The relay refuses model calls from outside (`403 network_not_allowed`). Tool calls go device → API and are not
fenced by the relay: an account that needs them fenced enforces IP rules in its own API. `restricted` in the device
file must be true exactly when the private rule restricts. Network ids `[A-Za-z0-9_.@-]{1,64}`; CIDRs IPv4 or IPv6.

## Budgets

```json
// private/agents/<kind>.json — each user, this streamline
{ "limits": { "tokensPerUserPerDay": 600000, "costPerUserPerDayUSD": 2 } }
// private/tenant.json
{ "budgets": { "warnAt": 0.8, "perUser": { "tokensPerDay": 3000000, "costPerDayUSD": 5 },
               "users": { "u-marco": { "costPerDayUSD": 10 } }, "roles": { "associate": { "costPerDayUSD": 40 } },
               "streamlines": { "register": { "costPerDayUSD": 200 } } } }
```

Per UTC day; the first cap reached refuses the call (`429 budget_exceeded`).

## Transcripts

Organization accounts keep each stream's record on the back end:
`private/tenant.json` `"transcripts": { "retentionDays": 365 }` (0 = until the account is deleted). Sealed values
are never in a transcript. Reading needs an admin or owner, or a server key with `transcripts:read`.
