### Why / What / How **Why:** the Microsoft app's three settings (`MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`, `MICROSOFT_TENANT_ID`) are shared by every developer machine that runs the Teams bot or the Microsoft connection. Copying them between machines by hand means pasting a secret through a terminal or chat. **What:** a small developer script, `python -m scripts.env_sync`, that compares chosen keys in env files on other machines (over SSH) with a local env file, and copies missing values only when asked. It never prints a value. **How:** - Reads each `--source` (`host:/path/.env`, or `local:/path`) as UTF-8, keeping only the allowlisted keys. Without `--key`, it uses the three Microsoft settings. Hosts named with `--windows-host` (default: `desktop`) are read through PowerShell, with the path quoted for its tokenizer, curly quotes included. - A dry run by default, reporting each key as `matches`, `different`, `missing at target` or `missing at source`. `--apply` writes missing values. Replacing a different non-empty value also needs `--replace`. Sources that disagree are rejected. An empty or missing source value never clears the target. - Which keys change, and whether that replaces a value, is decided from the target as read under its `.sync-lock`. A value another run wrote after the report therefore still needs `--replace`. - Writes go to a local file only, through an atomic replace. The previous file is first copied to `~/.local/state/autogpt-env-sync/` (or `$XDG_STATE_HOME`), a 0700 directory outside any repository, so `git add` cannot stage the copy. The edit is drafted there too. - A sync killed mid-write can leave only the lock and the `.env-sync-*` replacement beside the target, and `.gitignore` covers both. A leftover lock stops later runs with a message naming it. - Tracked files, symlinks and template env files (`.env.default`, `.example`, `.sample` and the like) are refused, by the dry run too. Suffixes match in any case. The tracked check ignores inherited `GIT_DIR`-style and pathspec variables, matches the name literally, and stops on any git answer it doesn't recognise. - `docs/engineering/env-sync.md` explains it. ### Changes 🏗️ - `backend/scripts/env_sync.py`, with tests in `env_sync_test.py` (CLI and reading) and `env_sync_apply_test.py` (writing a target). - `docs/engineering/env-sync.md`. - `.gitignore`: `.env-sync-*` and `*.sync-lock`. ### Agents and large language models used - Claude Code with Claude Opus 5.5: implementation, review fixes and tests. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] `pytest scripts/env_sync_test.py scripts/env_sync_apply_test.py`: 39 passed. They cover value selection and conflicting sources, the dry-run statuses and refusals, and Windows hosts with PowerShell quoting. They also cover non-ASCII values under an ASCII locale, `--replace`, and backups kept out of the repository and private. The race with another run, a sync killed at either file replacement leaving nothing `git status` lists, the stale-lock message, a failing read printing nothing of its output, and the refusal of symlinked, template and tracked targets (also in any case, under an inherited `GIT_DIR` or pathspec setting, with an option-like or pathspec-like name, or when git fails) are covered too. - [x] Ran the generated PowerShell read command against a Windows host with a path containing all five quote characters PowerShell ends a string at. - [x] Used it to compare the Microsoft settings across three machines without printing them. - [x] Black, isort, pyright and the secret scanners pass. #### For configuration changes: - [x] `.env.default` is updated or already compatible with my changes - [x] `docker-compose.yml` is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**): `.gitignore` gains two patterns for files a killed sync can leave. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (Claude Code) <[email protected]>
2.1 KiB
Syncing selected development settings
scripts/env_sync.py copies chosen keys from env files on your other machines
into a local one, over SSH. It never prints a value. Without --key, it syncs
the three settings the Microsoft app shares between the backend and the Teams
bot: MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET and
MICROSOFT_TENANT_ID.
From autogpt_platform/backend:
poetry run python -m scripts.env_sync \
--source 'mac:/path/to/backend/.env' \
--source 'desktop:C:\path\to\backend\.env' \
--target .env
This is a dry run: it reports each key as matches, different,
missing at target or missing at source. Add --apply to write the keys
missing at the target. Replacing a different value also requires --replace.
Sources with conflicting values are rejected: choose one explicitly. Empty or
missing source keys never clear the target. Use repeated --key KEY_NAME to
select another explicit allowlist. Name the SSH hosts that run Windows with
repeated --windows-host HOST; without it, desktop is the only one.
Applying checks the target again under a lock just before writing, so a value
another run added since the report still needs --replace. It then copies the
existing target to ~/.local/state/autogpt-env-sync/
($XDG_STATE_HOME/autogpt-env-sync/ when that is set), a directory only you
can read and outside any repository, so git add cannot stage the copy. It
prints the copy's path. The script never deletes these copies, and each holds
every setting the target had, so remove old ones you no longer need. Finally it
replaces the target atomically, keeping unrelated settings.
Tracked files, symlinks and template env files are rejected, by the dry run too. Only local targets are writable; remote environments stay unchanged. Choose a canonical source before enabling any scheduled sync.
A sync that is killed mid-write can leave <target>.sync-lock, and possibly an
.env-sync-* file, beside the target. This repository's .gitignore covers
both; for a target in another repository, add the two patterns to its
.gitignore. Later runs stop and name the lock; delete it once no sync is
running.