Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,19 @@ preferences are ignored and their files are left untouched. See the bundled
| GitHub Copilot CLI | `~/.copilot/ucode.env`, `~/.copilot/ucode-mcp-config.json` |
| Pi | `~/.ucode/pi-home/.pi/agent/models.json`, `~/.ucode/pi-home/.pi/agent/settings.json` |
| Cursor Agent | `~/.cursor/mcp.json` |
| Unity Gateway | `~/.ucode/managed-state.json`, `~/.ucode/managed-backups/` |
| Unity Gateway | `/etc/unitygateway/managed-settings.json` (Linux and macOS), `~/.ucode/refresh_ts.json`, `~/.ucode/managed-state.json`, `~/.ucode/managed-backups/` |

Workspace policy is root-owned but readable without sudo. Configure installs it only when the
workspace publishes a policy; launches check for updates every five minutes. Non-root launches
never invoke sudo for workspace policy: they warn about changes and keep the installed policy
until an administrator or IT provisioning run updates it. Each distinct pending change warns once.
The installed workspace is authoritative: selecting a different one fails launch instead of
falling back to unmanaged behavior.
Refresh timestamps and pending-change hashes live in user-owned `~/.ucode/refresh_ts.json`,
not in the protected policy file.
This policy is separate from agent-file backups and is not removed by `ug revert`.
See [managed workspace policy](docs/managed-workspace-policy.md) for permissions and trade-offs.
Managed policy headers must not contain secrets: the policy is readable by all local users.

### Codex desktop tracing

Expand Down
150 changes: 110 additions & 40 deletions docs/managed-workspace-policy.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Managed workspace policy

`ug configure` reads the workspace's published `CodingAgentConfig` through
`GET /api/ai-gateway/v2/coding-agent-configs` and installs it machine-wide at
`/etc/unitygateway/managed-settings.json` on Linux and macOS.
`GET /api/ai-gateway/v2/coding-agent-configs` and installs it at
`/etc/unitygateway/managed-settings.json` on Linux/macOS.

The installed file contains only:
The file contains only:

```json
{
Expand All @@ -13,40 +13,110 @@ The installed file contains only:
}
```

`config` is the raw remote configuration, not normalized agent settings. The empty object above
only illustrates the shape: a workspace without a published policy does not leave a policy file.
The directory is root-owned `0755` and the file is root-owned `0644`, so ordinary users can read it
without sudo. Authentication tokens and profiles stay in user-owned state.

**Never put secrets in managed policy headers.** The policy is readable by every local user, and
ug does not redact raw fields. Do not put passwords, bearer tokens, API keys, or session cookies in
`http_headers` or anywhere else in the configuration. Routing headers such as a workspace ID are
appropriate.

## Configure

- `ug configure` forces a fresh read of the workspace policy, so it never installs a stale one.
- Changed policy is written with sudo from a terminal. Where sudo cannot prompt (for example in a
script), ug uses non-interactive sudo and fails if a password would be needed.
- Root-run configure installs directly, without sudo.
- Unchanged policy writes nothing. A re-read that differs only in `retrieved_time` is not a change.
- A workspace that publishes no policy, or has the feature disabled, removes the installed file.
Configure decides the machine's workspace, so this also removes a policy installed for a different
workspace.
- If the installation or removal fails, configure stops with an error that says the machine-wide
file needs sudo. Rerun `ug configure` from a terminal where you can use sudo, or ask your IT team
or an administrator to run it.
- Windows has no machine-wide policy file. `UG_SKIP_OS_SETTINGS=1` and `ug configure --user` also
skip it, so no file is written.

Launches never install or remove the machine-wide policy. Per-user caching of the fetched policy
(`~/.ucode/managed-config.json`) is unchanged by this write path.

## Limits

- The policy describes one workspace for the whole machine. Configuring another workspace replaces it
for all users.
- `ug revert` removes the policy, with sudo. Without sudo it warns and keeps the file.
- Installation needs sudo on Linux and macOS. On MDM machines, ask IT to run the provisioning step
rather than attempting administrator-only configure yourself.
- Live sudo and ownership checks need a disposable clean machine, not a developer workstation.
`config` is the raw remote configuration, not normalized agent settings. The empty object
above illustrates the shape only: a workspace without policy does not create a policy file.
The installed directory is root-owned `0755`; the policy is root-owned `0644`, so ordinary
users can read without sudo. Authentication tokens and profiles remain in user-owned state.
Administrator-authored policy fields, including configured headers, are visible to all local users.

**Never put secrets in managed policy headers.** The current schema accepts arbitrary header
strings; ug cannot establish that a value is safe to publish. Do not put passwords, bearer tokens,
API keys, or session cookies in `http_headers` (including `Authorization`, `Cookie`, or secret
custom headers). Use per-user authentication helpers/credential storage instead. Routing headers
such as a workspace ID are appropriate. This requirement applies to the entire world-readable raw
configuration, not just headers; ug preserves raw fields rather than silently redacting policy.

## Configure and launch

- Configure forces a remote check and installs changed policy before using it. A privileged
installation failure is an actionable configure error.
- Launch reads installed policy and reuses a successful check for five minutes. Once expired,
it checks remote policy; indirect agent checks reuse the same timestamp.
- The installed policy's workspace is authoritative for launch and remote refresh. A different
workspace selected in user state or with `--workspace` causes a launch error before authentication,
bootstrap, cache reuse, or any remote request. This also applies to dry-run and `--skip-preflight`.
Without a user-selected workspace, launch uses the installed workspace. Explicit administrator
configure can replace the machine workspace; launch cannot switch it.
- Unchanged policy does not invoke sudo or touch installed bytes/metadata.
- Non-root launches never install or remove workspace policy, even in a terminal or with cached
sudo access. They do not run `sudo -n -l`, start a sudo worker, or request a password.
This avoids elevation attempts on MDM machines; IT/root provisioning owns the update.
- Explicit configure may request sudo in a terminal or use `sudo -n` in scripts.
Root-run configure/refresh installs directly without sudo.
- A deferred launch update warns: “Your workspace policy changed. Ask your IT team or an
administrator to apply the update on this machine.”
Launch continues with the matching installed policy. Without one, the fetched policy applies to
that run, but only if there is no installed policy for another workspace; a workspace mismatch is an
error, not unmanaged fallback. Checks remain throttled for five minutes, but the warning is shown
only once per distinct pending policy change, not at every expired check.
- No-config or feature-disabled removes a matching installed policy during configure or a root
refresh. A non-root launch warns and retains it.
- `ug configure` sets the machine's workspace: configuring a workspace that publishes no policy
also removes another workspace's installed policy. A launch never changes the machine's
workspace; selecting another workspace is refused.
- Fetch/authentication failures retain existing local fallback and do not advance the timestamp.
- Launch dry-run is local/offline. Configure dry-run may fetch to preview but does not install.

The one file holds one active workspace for the machine. Installing another workspace replaces
that policy for all users. Workspace URLs are canonicalized; a mismatched selection is refused,
not used to bypass machine policy. Agent-specific Claude/Codex OS-managed settings remain separate; their existing
conflict restrictions can still prevent launch even when workspace-policy refresh falls back.

## Refresh metadata

`~/.ucode/refresh_ts.json` is user-owned `0600`, keyed by canonical workspace URL. Each entry has
`last_checked_at`, `outcome` (`published`, `none`, or `feature_disabled`), and the installed-file
fingerprint. Deferred updates also record `pending_config_hash`, a SHA-256 digest of the canonical
workspace and fetched configuration, excluding `retrieved_time` and insensitive to JSON object
key order. No second policy copy is stored.

The pending hash suppresses repeat change warnings across TTL expiry, including pending policy
removal. A different pending config warns again. A successful installation/removal or a fetched
config matching the installed policy clears the hash. External MDM updates invalidate the file
fingerprint, so a user's next launch reconciles its own sidecar even if root used a different home.
Failed authentication/API reads preserve the pending hash and last successful check time.
Metadata is best-effort; deleting/corrupting the sidecar, failed writes, or concurrent launches can
cause a repeated warning.

An inode or mtime indicates a local write, not the last successful unchanged remote check.
The sidecar allows throttling without updating the root-owned file. Its fingerprint (existence,
device, inode, size, mtime, ctime) invalidates a warm check after local replacement/edit.
These timestamps are an optimization, not a tamper-proof enforcement mechanism.

The API's `retrieved_time` changes on reads and is excluded from drift comparison. Its stored
value may therefore predate the latest check; use `last_checked_at` for refresh information.
All other raw fields participate in comparison. Policy is otherwise preserved when installed.

## Migration and limits

On Linux and macOS, the per-user `~/.ucode/managed-config.json` is no longer read and is never
promoted to a root-owned policy.
Run `ug configure --workspace YOUR_WORKSPACE` to retrieve and install authoritative policy.
`ug revert` removes the installed policy with sudo; without sudo it warns and keeps the file.
No policy is written when the workspace has none. On MDM machines, ask IT to run root provisioning
instead of attempting administrator-only configure yourself.

The five-minute TTL avoids every-launch request latency but delays detection of remote changes.
Warning/fallback prioritizes availability over strict enforcement of the newest policy.
Privileged writes are supported on Linux/macOS only. Real sudo/password and ownership integration
checks require a disposable clean machine, not an existing developer-managed workstation.

Where the machine cannot hold the policy (Windows, `UG_SKIP_OS_SETTINGS=1`, or `ug configure
--user`) and none is installed for the workspace, ug keeps its earlier behavior unchanged: the
fetched policy is cached per user in `~/.ucode/managed-config.json`, a launch reuses it for five
minutes, and a failed fetch falls back to it. Windows support for the installed policy is a
follow-up. A non-root Linux or macOS launch with nothing installed applies the fetched policy to
that run without installing or caching it, so a first launch before any `ug configure` is still
managed. An installed policy still wins: with `UG_SKIP_OS_SETTINGS=1`
a developer's launch reads it read-only, never writes OS-managed files, and builds only user-owned
settings. That is the supported developer mode on an MDM machine whose administrator ran `ug configure`.

There is no refresh-interval setting in the currently supported `CodingAgentConfig` shape.
The five-minute launch-check default is retained; the installed workspace selects the remote source.

For fleet-managed installations, schedule root/MDM provisioning about every **four hours**
(`14400` seconds) rather than once daily. This bounds the usual stale-policy window without requiring
developer elevation; offline/sleeping machines can still lag. This is separate from the five-minute
developer check interval. The PR's baseline has no MDM scheduler, and current main's
`scripts/mdm-bootstrap.sh` is a one-shot bootstrap with no 24-hour refresh default. The deployment's
actual scheduler must set the four-hour interval; this change does not invent a background daemon.
Loading
Loading