From aa7910628aca66c9de8d1bd179d14d2b062ddc5b Mon Sep 17 00:00:00 2001 From: "openhands-release-bot[bot]" <290150379+openhands-release-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:33:16 +0000 Subject: [PATCH] docs(cookbook): sync from OpenHands/enterprise-cookbook Syncs the Enterprise Cookbook tab with OpenHands/enterprise-cookbook@942452c44b078b19998188bf538f6e8d91961157. Every file under `cookbook/` and the Enterprise Cookbook tab in `docs.json` are generated from that repository. Change them there, not here; edits made directly in this repository are overwritten by the next sync. _Opened automatically by the docs-publish workflow in OpenHands/enterprise-cookbook ([run](https://github.com/OpenHands/enterprise-cookbook/actions/runs/40997989667))._ --- cookbook/aws-secrets-sync.mdx | 263 ++++++++++++++++++++++++++++++++++ cookbook/index.mdx | 4 + docs.json | 1 + 3 files changed, 268 insertions(+) create mode 100644 cookbook/aws-secrets-sync.mdx diff --git a/cookbook/aws-secrets-sync.mdx b/cookbook/aws-secrets-sync.mdx new file mode 100644 index 000000000..f09e74a0d --- /dev/null +++ b/cookbook/aws-secrets-sync.mdx @@ -0,0 +1,263 @@ +--- +title: AWS Secrets Sync +description: Sync AWS Secrets Manager values into OpenHands personal or organization secrets. +icon: key +--- + +{/* GENERATED from OpenHands/enterprise-cookbook@main (aws-secrets-sync/README.md). Edit the source, not this file. */} + + + +A Python script you run on your own scheduler. It reads explicitly mapped AWS +Secrets Manager values, writes them through OpenHands secrets APIs, and updates +them when AWS rotates them. AWS is the working example: you can adapt the source +reader to another secrets manager. Nothing is installed in OpenHands. + +## How It Works + +```mermaid +sequenceDiagram + participant S as Scheduler + participant P as Sync script + participant J as SQLite journal + participant A as AWS Secrets Manager + participant O as OpenHands API + S->>P: Run with mapping and OH_API_KEY + P->>O: Verify key binding; list secret names + loop Each mapping + P->>J: Read checkpoint + P->>A: Describe AWSCURRENT version + alt Missing or authorized changed version, with --apply + P->>A: Read that exact version + opt Replacing a shared value + P->>J: Record delete_pending + P->>O: DELETE secret + P->>J: Record deleted after acknowledgment + end + P->>J: Record pending + P->>O: POST create or personal upsert + P->>J: Record applied after acknowledgment + end + end +``` + +The script creates missing entries, skips unchanged AWS versions, and updates +changed versions only for mappings already owned by its journal. Unknown existing +names or edited ARN/field mappings are refused; use a new destination name or +[operator recovery](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/RECOVERY.md). Source deletion and removed mappings never +cause destination deletion. + +## Prerequisites + +- Python 3.10+ on Linux or macOS; `boto3` and `requests` from `requirements.txt`. +- AWS credentials through the [Boto3 credential chain](https://docs.aws.amazon.com/boto3/latest/guide/credentials.html). + Grant `DescribeSecret` and `GetSecretValue` on the mapped ARNs; see the + [IAM example](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/iam-policy.example.json) and [AWS access documentation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access.html). + Customer-managed encryption keys may require `kms:Decrypt`. +- An organization-bound OpenHands API key injected as `OH_API_KEY`. Shared-secret + writes require owner/admin permission. Keep credentials out of mappings and + command arguments. `GET /api/keys/current` returns organization and user IDs; + verify they match the intended account and organization before configuring them. + +## Run It + +From this directory, install dependencies: + +```bash +python -m pip install -r requirements.txt +``` + +Set the mapping's HTTPS origin, organization UUID, exact AWS ARN, and destination +name (a valid environment-variable name). `json_field` selects one top-level string +field; omit it for the whole string. Binary values are unsupported. + + + + Copy [mapping.example.json](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/mapping.example.json) to `mapping.local.json` and + edit it. Use the same journal for preview, initial import, and recurring sync. + + ```bash + python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite + ``` + + ```text + mapping 1: would create (value not read) + ``` + + ```bash + python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite --apply + ``` + + ```text + mapping 1: create acknowledged + ``` + + After the source version changes, run: + + ```bash + python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite --sync --allow-delete-recreate --apply + ``` + + ```text + mapping 1: delete/recreate acknowledged + ``` + + + This command deletes and re-creates shared values, including on newer servers. + If creation fails, the secret can remain missing for all members until recovery. + Read [Updates and Recovery](#updates-and-recovery) before scheduling it. + + + + + Copy [mapping.personal.example.json](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/mapping.personal.example.json) to + `alice.local.json`. Supply the intended user's own org-bound key and + `expected_user_id`; an administrator key does not impersonate them. Use a separate + mapping and persistent journal for each user. + + ```bash + python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite + ``` + + ```text + mapping 1: would create (value not read) + ``` + + ```bash + python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite --apply + ``` + + ```text + mapping 1: create acknowledged + ``` + + After the source version changes, run: + + ```bash + python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite --sync --apply + ``` + + ```text + mapping 1: personal upsert acknowledged + ``` + + + +Omit `--apply` to preview without reading values. Unchanged versions print: + +```text +mapping 1: checkpoint unchanged (not value-verified) +``` + +Your scheduler can run the recurring command weekly with absolute paths, injected +credentials, and the same persistent journal. Its directory must be private +(`chmod 700`). Run one exclusive writer per destination and alert on any nonzero +exit; successful mappings remain committed when another fails. + +## Secret Name Collisions + +On the tested API, personal secrets belong to a user within the effective +organization. Personal and shared names are separate namespaces; neither create +endpoint rejects a name used in the other scope. The personal value keeps the bare +name; the shared value receives a suffix (`_2`, `_3`, …) +in that user's conversations and `GET /api/v1/secrets/search`. The stored shared +name remains unchanged in the organization listing. + + + An organization sync cannot see members' personal secrets. A member with a + matching personal name keeps their own value under that name; rotating the shared + value only changes their suffixed variable, such as `NAME_2`. + + +Personal preflight rejects mapped names that match shared names as listed in the +search results. A shared secret created later can appear already suffixed, so the +next sync does **not** detect that base-name collision. Use team/system prefixes +and reserve managed names across scopes; this script cannot enforce that policy. + +## Updates and Recovery + +Personal values use upsert. This recipe rotates shared values with DELETE then +POST, only with `--sync --allow-delete-recreate`. It validates the new AWS value +before deleting anything. The existing organization metadata PUT cannot replace +values; the newer value-replacement API is separate (see [Compatibility](#compatibility)). + + + After DELETE succeeds, the shared entry is absent until POST succeeds. New + conversations started in that window lack the shared value. A failed or + unacknowledged POST can leave it missing for all members until an operator + completes [recovery](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/RECOVERY.md); it is not just a brief availability gap. + A timeout can also hide a successful write. The script blocks uncertain mappings + instead of guessing or automatically retrying them. + + +New conversations receive updated values; running conversations can retain old +values. Credential revocation is a separate process. Keep exclusive control of +managed names: the operations used here have no conditional-write contract, and +the local journal lock cannot coordinate other clients. Out-of-band destination +value changes are not detected. + +| Journal Status | Action | +| ---------------------------- | --------------------------------------------------------------------------------------------------------- | +| `applied` | Normal sync can continue | +| `deleted` | Acknowledged shared deletion, no POST attempted; rerun the shared sync command to restore an absent entry | +| `pending` / `delete_pending` | Uncertain outcome; follow operator recovery before resuming | + +A `deleted` checkpoint also blocks if the entry is unexpectedly present; reconcile +it before proceeding. Do not erase the journal to bypass uncertainty. Values and +tokens are not intentionally logged or stored in it; disable HTTP/SDK wire logging +in your runner. + +## Adapt to Another Vault + +In [sync_secrets.py](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/sync_secrets.py), replace `current_version()` and +`read_value()` with your vault's metadata/version and value API calls. Adjust the +client factory, ARN-to-region parsing in `sync()`, mapping validation, and source +error handling. Keep a stable source ID and version token; read the exact requested +version and fail safely if it changes between metadata and value reads. + +Keep the OpenHands adapters and journal for the destination side. Other vaults +are extension points, not included integrations; qualify their authentication, +versioning, retries, and synthetic-secret delivery before use. + +## APIs Used + +| Endpoint | Used For | +| --------------------------------------------------- | -------------------------------------------------------- | +| AWS `DescribeSecret` / `GetSecretValue` | Locate and read the exact `AWSCURRENT` version | +| `GET /api/keys/current` | Verify effective/bound organization and personal user | +| `GET /api/organizations/{org_id}/secrets` | List shared names | +| `POST /api/organizations/{org_id}/secrets` | Create shared values | +| `DELETE /api/organizations/{org_id}/secrets/{name}` | Remove the old shared entry before re-creation | +| `GET /api/v1/secrets/search` | List personal/shared names visible to the user | +| `POST /api/v1/secrets` | Create or upsert the authenticated user's personal value | + +## Compatibility + +The [maintainer review](https://github.com/OpenHands/enterprise-cookbook/pull/29#pullrequestreview-5689160630) +validated this script at `2e83432` on **Enterprise 1.74.0**, using simulated AWS +(moto) and real OpenHands APIs and agent consumption of rotated values. Qualify +your deployment before importing real credentials; this is not a minimum-version +or blanket compatibility guarantee. + +**Enterprise 1.81.0 added atomic shared-value replacement** through separate +`GET .../secrets/{name}/version` and `PUT .../secrets/{name}/value` endpoints. +This recipe does **not** use or qualify them and still deletes/re-creates shared +values on newer servers. Adopting that API requires a separately tested adapter +and conditional-version/retry handling; the ordinary metadata PUT remains +metadata-only. Inspect your deployment's `/openapi.json` before choosing a path. + +## Related + + + + Managing and using secrets + + + + Exact version reads + + + + A separate delivery scope + + diff --git a/cookbook/index.mdx b/cookbook/index.mdx index a2f4fbbb5..9b18ad0ea 100644 --- a/cookbook/index.mdx +++ b/cookbook/index.mdx @@ -67,6 +67,10 @@ Observe conversations and react to their state. Inject credentials and manage identity. + + Sync AWS Secrets Manager values into OpenHands personal or organization secrets. + + Configure GPG commit signing on every conversation with a SessionStart hook that imports a key from a custom secret. diff --git a/docs.json b/docs.json index 7c7506766..b37f3efab 100644 --- a/docs.json +++ b/docs.json @@ -600,6 +600,7 @@ { "group": "Secrets & authentication", "pages": [ + "cookbook/aws-secrets-sync", "cookbook/gpg-commit-signing", "cookbook/per-conversation-secrets", "cookbook/service-account-github-pat"