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

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

Raw Markdown the file as the skill ships it All pages ↓

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)

kindWhenFields
inviteThe 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.—
oidcThe 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
connectionNo IdP; signing in to the named system opens the session.connection
demoDemo 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.

"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." } }
]
typeMeaning
oauthThe 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.
passwordA 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-keyThe person's own key for that system, on the same kind of card (designed).
nonePublic, read-only tools only.

oauth connections take these too (all optional):

FieldMeaning
scopeperson (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.
authorizationServerThe issuer, when the MCP server publishes no protected-resource metadata.
scopesOAuth scopes to ask for, when the server advertises none.
resourceThe RFC 8707 audience (mcp[].resource works too; the connection's wins).
signIn.helpOne 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." }.

// 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

// 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.