openstation

Reference

people.yaml

Map channel handles to people, roles, and explicit overrides.

The roster, at .openstation/people.yaml. It maps a channel handle — a Telegram user id, a Slack member id, an email address — to a person, their role, and optionally a profile.

The two do different jobs, and keeping them apart is deliberate:

  • role says who they are. It decides admission — a channels: entry with roles: [admin] admits only those — and nothing else.
  • profile says how the agent behaves for that person — charter, tool ceiling, model. It carries no authority; that is claudeSettings, which resolves separately. Assigning one is an explicit act, and it then follows them into every space.

A role never selects a profile. If it did, anyone holding a privileged role would carry its capabilities into every room they are in.

The file is optional. Without it, identity is passthrough: every caller in the process is the one role the daemon started with (--as, default member). With it, each caller gets their own.

The scaffold doesn't write this file. openstation members add <id> creates it on first use and appends to it after — see members add below.

Shape

defaultRole: member          # optional; omit to refuse contacts not listed
members:                     # optional, default []
  - id: leon                 # required: internal, channel-independent id
    name: Leon               # optional: display name
    role: owner              # required: admission only
    profile: owner           # optional: -> .openstation/profiles/owner.md, follows them everywhere
    claudeSettings: owner-gate # optional: -> .claude/settings.owner-gate.json, also follows them
    contacts:                # optional, default {}; empty until a handle is added
      telegram: "123456789"
      slack: "U01234567"
  - id: dad
    role: member
    contacts:
      telegram: ["456", "789"]   # one person, several handles on a channel
  - id: alice                # add member's own output: no contacts yet
    role: owner

Fields

Field Required Meaning
defaultRole no Role for a contact not in members. Omit it and unknown contacts are refused — the agent answers "You're not authorized to use this agent." and no turn runs
members[].id yes Internal id, stable across channels. What audit attribution carries
members[].name no Display name, passed through as the principal's displayName
members[].role yes Who they are. Drives admission (channels[].roles:) and the advisory grant. Must match ^[A-Za-z0-9][A-Za-z0-9_-]*$, and — once the workspace has a roles.yaml — must be declared there with scope: person or both
members[].profile no Profile they always run, overriding the space's. Omit and they get the space's profile, or the agent's default
members[].claudeSettings no Settings label they always run under, overriding the space's. Its own axis — a person can be given a wider gate without a different voice
members[].contacts no channel: handle or channel: [handle, …]. Defaults to {} — a member with none simply resolves nothing until you add one by hand. Handles are matched verbatim — never trimmed, never lowercased

Channel keys are the connector names: telegram, slack, email, repl.

Creating and extending the file: members add

bun packages/cli/src/index.ts members add alice -d . --name Alice --role owner
added member "alice" to .openstation/people.yaml

First call writes the stub — the header comment and commented example above, plus defaultRole: member — then adds the entry; later calls append to the existing file, comments and formatting intact. --role defaults to the roster's own defaultRole. Either way the role needs a .openstation/profiles/<role>.md; members add scaffolds one if it's missing, the same rule workspace create follows for -r/--role. A duplicate id, or an id/role that isn't a safe filename, is refused before anything is written.

The member it adds has no contacts: — wire up a channel handle by hand afterward, the same as hand-editing the file directly.

A named profile must exist

A profile: with no .openstation/profiles/<name>.md is a refusal, not a downgrade — the caller sees "This agent isn't configured for your role yet." The same holds for a profile a channels: entry names, and for the agent's own default. See permissions.

Adding role: owner needs no file: a role is not a profile.

Load-time errors

The file is validated at boot; an invalid roster stops the daemon rather than quietly granting the wrong role.

Two members claiming one handle — resolution would depend on file order:

openstation people <workspace>/.openstation/people.yaml is invalid: members.1.contacts.telegram:
handle "123" on channel "telegram" is already claimed by member "leon"

A duplicate id and a role that isn't a safe filename are refused the same way. A member with no contacts: is valid — it just resolves nothing until one is added. A missing file is not an error — it means no roster is declared.

A role absent from roles.yaml is refused too, in a workspace that has one. Until 2026-09-02 a role here was checked for name pattern only, so role: admni loaded cleanly and then silently dropped that person to defaultRole, admitting them nowhere.

What this is not

Not a tool allow-list here: this file assigns roles, it does not define them. What a role grants lives in roles.yaml, and the enforced gate is still the agent's .claude/settings.<label>.json — now compiled from that roster rather than hand-authored. Not identity federation either — one person's handles are linked here by hand, per workspace; there is no cross-channel directory.

View Markdown source on GitHub ↗