SysEngineering

Integration and automation tooling for Hillspire IT Operations.

Hillspire supports a family office spanning ~30 legal entities, 21 Google Workspace organizations, one shared Okta tenant and a long tail of SaaS. Most of the work in this repo exists because a task that is trivial for one organization becomes a genuine engineering problem when it has to be right for all of them, every time, without a human remembering which console to open.

So this is not a scripts folder. It is a set of small, single-purpose services and jobs that each keep one thing true:

A plain-English catalogue of everything below, written for people rather than engineers, lives in Confluence: IT Projects & Operations → 3.I Coding Documents → IT Automation & AI Tooling. A formatted PDF edition is at docs/AUTOMATION_AND_AGENTS_CATALOG.pdf.


What’s here

Agents and Slack tools

Integration What it does Status
harry_helpdesk_agent Harry. Natural-language Slack agent for software access. Files JSM requests into the helpdesk team’s two request types, answers how-to questions from 25 curated ITOPS guides, walks people through setup step by step with the guides’ own screenshots, enforces the KnowBe4 training gate and the team’s per-app justification bar, and raises a diagnosed ticket with a human when it cannot finish the job. Claude Opus 5 tool loop, 11 tools, no keyword matching anywhere. Live
google_workspace_admin_assistant Greg. Slack agent answering plain-English Workspace admin questions across all 21 organizations. Claude Opus 5 tool loop over ~20 read-only tools. Strictly read-only, enforced by tests. Live
slack_helpdesk_module Helpdesk Tasks. Mailbox and offboarding actions from one Slack modal, routed to the right Workspace tenant by email domain. Cloud Tasks worker, fail-closed super-admin guardrail, bulk CSV mode. Live
slack_helpdesk_module/audit_publisher Every helpdesk action → BigQuery → a monthly Confluence page, rebuilt from source each run. Live

Scheduled jobs

All run unattended on GitHub Actions. Each exits non-zero when something needs a person, so a failed run is itself the alert.

Integration Cadence What it keeps true
outage_slack_ping 5 min Sally. 16 vendor status feeds → Slack, on incident open and resolve only.
okta_audit_log 15 min Okta System Log → a running Confluence audit page (groups, apps, assignments, admin grants).
okta-api-ping hourly + ~10 days Ava. Keeps 16 HR→Okta provisioning tokens from dying at Okta’s 30-day idle limit; logs every key created or revoked.
domain_expiry_sync monthly Registrar (GoDaddy, RDAP fallback) → the Expiration Date on each domain in JSM Assets.

Identity engineering — run on demand

Integration What it does Status
okta_license_cleanup Buckets every Okta account against HR data to find genuinely reclaimable seats. Read-only; deactivation is separate, opt-in tooling with an undo. Complete
okta_entity_status Stamps a for-profit / non-profit attribute on every active user from their login domain. Complete
okta_profile_sync One-time profile backfill for accounts predating the TriNet integration. Complete
okta_group_scrape Read-only CSV export of every Okta group and membership. Complete

In flight on feature branches

Not yet merged to main. Check out the branch to see the code and its README.

Integration Branch What it does
carl_contracts_agent feat/carl-contracts-agent Carl. Private, DM-only Slack agent for read-only application, executed-contract, and aggregate account-activity questions from 1Password SaaS Manager (Trelica). Three enforced data roles, exact feature-gated read scopes, and no contract documents or unsupported actual-spend claims.
okta_title_sync + slack_title_audit + _shared feat/okta-title-nudge Corrected every Okta job title ahead of Slack’s switch to SCIM-managed titles, then forced the push to land tenant-wide — plus a read-only audit proving which users it reached. Also carries the shared HTTP/secrets/Slack/Okta/report modules.
okta_policy_replicate feat/okta_policy_replicate Copies a parent org’s Okta security policies into a new spinoff tenant, remapping every cross-org ID by name. Password policy applied live Aug 2026.

Parked

Kept for reference; not being worked on.

Integration Why it stopped
chief_of_staff_assistant Per-user calendar/inbox Slack assistant. Validated in sandbox; pilot paused.
phishing_check_assistant (feat/phishing-check-assistant) Self-serve phishing triage POC. Dropped — infosec already runs an equivalent capability.
okta_helpdesk_agent (feat/okta-helpdesk-agent) The original /helpdesk POC. Superseded by Harry, which shipped instead.

Endpoint and web

Path Contents
sophos/ Sophos Endpoint removal, install, and migration-validation PowerShell.
windows/ Windows imaging, taskbar management, and endpoint helper scripts.
web_dev/ Rise web properties and Apps Script widgets — fellow directory, events calendar, dynamic handbook, newsletter and birthday widgets.

How these are built

Four conventions run through everything here. They are worth knowing before adding an integration, because they are the reason the tools behave predictably.

Secrets never live on disk. Scripts only ever read process.env. The invocation decides where the value came from — op run with 1Password on a laptop, Actions secrets in CI, Secret Manager bound at container start on Cloud Run. The same script runs unchanged in all three, and nothing has to be edited per environment.

Employee data stays outside the working tree. HR exports and run reports contain names, emails, titles and employee IDs. A tracked .gitignore only protects the branches it exists on — a file protected on a feature branch is untracked and unignored on main, one git add -A from being committed. So integrations default both their input and their output to ~/Documents/hr-exports/<integration>/, outside the repo entirely. The root .gitignore is a second line of defence, not the first.

Read-only tools have no write methods. Where a tool is meant to read, the client it uses has no write capability at all — the shared Slack client has no post method, the Workspace assistant has no mutating call path, and a unit test fails the build if a non-.readonly scope is ever added. It is a property of the code, not a rule someone has to remember.

Anything that writes, dry-runs first. Every write tool defaults to (or prominently offers) --dry-run, is idempotent so a partial failure just needs another pass, and drops a JSON audit log plus a Confluence-ready report next to the run. okta_policy_replicate inverts the default and requires --apply, because it writes a tenant’s authentication policy rather than a data field.


Working in this repo

Layout

integrations/<name>/     one integration per folder
  README.md              what it does, setup, flags, and what bites
  CLAUDE.md              working brief: live state and decisions (where present)
  HANDOFF.md             operational state for deployed services (where present)
  .env.example           required config, with no real values
.github/workflows/       the scheduled jobs
docs/                    repo-wide documentation

Read a deployed integration’s HANDOFF.md before touching it — it holds the hard-won operational facts (live revision, required Cloud Run flags, gotchas).

Local setup

Python integrations expect a virtualenv at the repo root; Node integrations use Node 18+ and install per folder.

# Python (3.13)
python3.13 -m venv .venv
.venv/bin/python -m pip install -r integrations/<name>/requirements.txt

# Node
cd integrations/<name> && npm install

Several Node tools deliberately use only Node built-ins and need no install at all.

Running something that touches production

npm run dry-run                              # or: python <script>.py --dry-run
op run --env-file=./secrets.env.tpl -- npm run run-live

Read the dry-run output before the live run. Mint a fresh API token for the run and revoke it when you are done — that revocation, not deleting a file, is the control that matters.

Git workflow

Short-lived feat/ or fix/ branches, one per change, merged to main through a PR, branch deleted after. No long-lived development branch.

git switch -c feat/thing origin/main
# ... work, commit ...
git push -u origin feat/thing && gh pr create

Because the repo is worked on from more than one machine, push before you leave one and pull before you start on the other.


Contributing an integration

  1. integrations/<name>/ with a README.md that covers what it does, setup, every flag, and — most usefully — the things that bite.
  2. .env.example listing every variable, with no real values.
  3. Read config from process.env only. Never write a token to disk.
  4. Default any PII output to ~/Documents/hr-exports/<name>/.
  5. Dry-run mode first, and make it idempotent.
  6. If it is scheduled, add the workflow to .github/workflows/ and make it exit non-zero when a human needs to look.