office
Agent-first CLI for managing seat assignments and meeting rooms across
office floor plans. Floor layouts are hand-traced SVGs; people come from
BambooHR; assignments live in a Google Sheet (v1) or DynamoDB (v2). The
CLI exposes the same operations as the Slack /whereis command and the
web map.
Status — v0.8.0. Stages 1–8 of the v1 seating system are in: floor SVG parser/validator, CSV / Google Sheets / DynamoDB-backed assignment store with append-only audit log, BambooHR-backed
EmployeeDirectorywith the auto-vacate killer feature, CLI verbs (floors,seats,whereis), a Slack/whereisslash-command listener, a search-first web map, effective-date enforcement, SSO + role-aware redaction, and bi-directional Sheets ↔ Dynamo sync. See issue #1.
Naming surfaces
office uses three different identifiers across packaging surfaces. Mind
the split — do not blanket-replace one token across the codebase.
| Surface | Value |
|---|---|
| GitHub repo | agentculture/office-agent |
| PyPI distribution | office-cli |
| Python package | office_cli |
| CLI binary | office |
| Error class prefix | Office |
Install
uv tool install office-cli
office --version
For the full setup walkthrough — including how to opt into each optional
backend (Sheets, DynamoDB, BambooHR, Slack, web map, SSO) — see
docs/setup.md.
Use
office learn # self-teaching prompt
office explain seats # markdown docs for any verb
office whoami --json # auth probe (stub)
# v0.1.0 seating verbs:
office floors list --json
office floors validate floors/tlv-floor-5.svg
office seats list --vacant
office seats assign 5-T-01 alice@example.com
office seats move 5-T-02 alice@example.com
office seats history 5-T-01 --json
office whereis alice@example.com
office reads data/offices.yaml, floors/, and seats/ from the
current working directory. Override with --data-dir DIR or
OFFICE_DATA_DIR=DIR.
Storage backend
office defaults to CSV-backed storage under seats/ (assignments.csv,
audit-log.csv). To use Google Sheets instead:
pip install office-cli[sheets]
export OFFICE_STORE=sheets
export OFFICE_SHEETS_ID=1abc...
export OFFICE_SHEETS_SA=/path/to/service-account.json
…or declare it in data/offices.yaml:
storage:
type: sheets
sheets:
spreadsheet_id: "1abc..."
service_account: "data/sheets-service-account.json"
cache_ttl_seconds: 300
Reads honor a 5-minute TTL cache; writes invalidate it. See
docs/architecture.md for the full storage contract.
People directory (BambooHR)
office defaults to a no-op StubDirectory that trusts whatever email
it receives. Switch to BambooHR for the auto-vacate killer feature
(seats render as vacant automatically when an employee is offboarded).
Note: the BambooHR backend is gated off by default. Set
OFFICE_BAMBOOHR_ENABLED=1in addition to the config below; without it, even valid BambooHR settings silently fall back to the stub directory (with a one-line warning to stderr).
pip install office-cli[bamboohr]
export OFFICE_BAMBOOHR_ENABLED=1 # required: opt in to the gated feature
export OFFICE_DIRECTORY=bamboohr
export BAMBOOHR_SUBDOMAIN=tipalti
export BAMBOOHR_API_TOKEN=... # env-only — do not commit
…or declare the public bits in data/offices.yaml and keep the token
in the env:
directory:
type: bamboohr
bamboohr:
subdomain: tipalti
cache_ttl_seconds: 300
The directory affects rendering only; assignments stay in the store unchanged. Re-activating an employee in BambooHR restores their seat without any write.
Slack /whereis
Run a Slack /whereis slash command backed by the same SeatService:
pip install office-cli[slack]
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_APP_TOKEN=xapp-...
office slack-serve
Required Slack app scopes:
commands— to register/whereis.users:read.email— forusers.infoto returnprofile.email.chat:write— to post the ephemeral response.
Three invocation shapes:
/whereis— looks up the caller's own seat./whereis @user— Slack mention; resolves the user's email./whereis email@domain— plain text fallback.
A trailing YYYY-MM-DD token on any of the three shapes filters by
effective date — e.g. /whereis alice@x 2026-07-01 returns Alice's
seat as of that date.
Responses are ephemeral by default — only the caller sees them.
hidden=TRUE seats render as "occupied (private)" until role gating
(Stage 7) lifts the filter for privileged callers. Setting
OFFICE_WEB_BASE_URL to your office serve deployment adds an
"Open map" deep-link button to the response.
Effective-date windows
Assignments can carry an effective window so the seat map renders "as of" any date:
office seats assign 5-T-01 alice@example.com --from 2026-07-01 --until 2026-12-31
office whereis alice@example.com # default = today
office whereis alice@example.com --as-of 2026-07-15 # inside the window
office seats list --as-of 2026-09-01
The web map honors ?asOf=YYYY-MM-DD in the URL —
http://localhost:8000/offices/tlv/floors/tlv-floor-5?asOf=2026-07-15
shows the same view, deep-linkable.
effective_from / effective_until are stored as YYYY-MM-DD (date
precision); last_updated and audit-log timestamps stay full ISO-8601.
Empty bounds mean "always begins" / "no end".
DynamoDB backend + Sheets sync
office ships three storage backends behind a single Protocol: CSV
(default), Google Sheets, and DynamoDB. They are interchangeable
runtime backends; pick one via OFFICE_STORE or
storage.type in data/offices.yaml.
pip install office-cli[dynamo]
export OFFICE_STORE=dynamo
export OFFICE_DYNAMO_ASSIGNMENTS=office-assignments
export OFFICE_DYNAMO_AUDIT=office-audit-log
export OFFICE_DYNAMO_REGION=us-east-1
# AWS creds via AWS_PROFILE / IAM role / standard chain
office seats list
YAML:
storage:
type: dynamo
dynamo:
table_assignments: office-assignments
table_audit: office-audit-log
region: us-east-1
cache_ttl_seconds: 300
The office-assignments table is keyed on seat_id. The
office-audit-log table is keyed on seat_id (PK) + timestamp
(SK) so re-running migrations doesn't duplicate audit rows.
One-shot import/export
# Bootstrap Dynamo from Sheets:
office seats migrate --from sheets --to dynamo --dry-run # preview
office seats migrate --from sheets --to dynamo
# Snapshot Dynamo back to Sheets for offline review:
office seats migrate --from dynamo --to sheets --audit-append
Bi-directional sync (Sheets ↔ Dynamo)
Sheets stays the human-friendly editor; Dynamo is the runtime read
path. Run office seats sync periodically (cron / GitHub Action) to
reconcile both sides via last-write-wins on last_updated:
# Pick the tie-breaker side when last_updated matches but content
# diverges (rare). The reconcile is idempotent — re-running converges.
office seats sync --primary sheets --dry-run
office seats sync --primary sheets
The CLI is the supported sync entry point; there is no always-on daemon (a Stage-9+ addition if needed).
SSO + roles
The web frontend can be gated behind your IdP via OIDC. Three roles
are recognized: viewer (default — sees hidden=TRUE seats as
"occupied (private)"), editor (HR/IT — full details on hidden
seats), and planning (facilities — same as editor in v1).
pip install office-cli[sso,web]
export OIDC_ISSUER=https://your-idp.example.com
export OIDC_CLIENT_ID=office-agent
export OIDC_CLIENT_SECRET=xxx
export OIDC_REDIRECT_URL=https://office.example.com/auth/callback
export SESSION_SECRET=$(openssl rand -hex 32)
office serve --port 8000
Role mapping lives in data/offices.yaml under a top-level roles:
block:
roles:
editor:
- "hr-it@tipalti.com"
- "alice@tipalti.com"
planning:
- "facilities@tipalti.com"
Anything not listed is viewer — including unmatched authenticated
users.
When the OIDC env vars are unset, office serve runs in
auth-disabled mode: no redirects, no session middleware, every
request is anonymous. This is the default for local dev. An optional
X-Test-Role: editor request header drives role-aware behavior in
that mode (useful for curl smoke tests). The header is only
honored when OIDC is disabled.
The CLI is operator-only and always unrestricted (no role flag).
Slack /whereis resolves the calling user's role from their email
via the same roles map.
Adding a new floor
- Trace the floor in Inkscape following
docs/tracing-guide.mdand the SVG ID contract inCLAUDE.md. Save as Plain SVG. - Drop the SVG into
floors/as<office>-floor-<N>.svg. - Add an entry to
data/offices.yamlwith cluster capacities and any named rooms. - Verify:
office floors validate floors/<file>.svg. Errors fail the command; warnings (e.g. cluster-capacity mismatches) are informational.
Develop
uv sync
uv run pytest -n auto -v
uv run office --version
uv run python -m office_cli
License
MIT — see LICENSE.
Metadata
Release files for office-cli 0.19.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| office_cli-0.19.0.tar.gz | 339.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| office_cli-0.19.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 494.3 kB
Release files / office_cli-0.19.0.tar.gz
| Download URL | office_cli-0.19.0.tar.gz |
|---|---|
| Size | 339.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3d2597870d913338a2c3415485b3e83befb79e412f40863a3e40a0b4b2186a78
|
|
BLAKE2b-256 checksum How to use checksums |
c5dd641854e0dfe9c037d776f64e7a80a1138587e5f7a0d77f2a547d00c4510e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / office_cli-0.19.0-py3-none-any.whl
| Download URL | office_cli-0.19.0-py3-none-any.whl |
|---|---|
| Size | 155.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
877ad0db6b4e11cc2fa01d81df913b92926dd0a40c033bb496bfbf288cd99bbc
|
|
BLAKE2b-256 checksum How to use checksums |
19471b7cbf43fdcf844b24e6d322bb84a15639fe1496176b400c88073ca409bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for office-cli 0.19.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.