> ## Documentation Index
> Fetch the complete documentation index at: https://opensre.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync to your own object store

> Carry your conversations and memory between machines, using a store you own.

opensre keeps everything on your laptop by default. Turn on remote sync and it
also mirrors your conversation history and memory to an object store **you**
own, so a second machine picks up where the first left off.

Your cloud credentials are read from the usual places — environment, profile,
SSO session. opensre never stores them.

## What is mirrored

|                    |                                               |
| ------------------ | --------------------------------------------- |
| Mirrored           | `~/.opensre/sessions/` — your conversations   |
| Mirrored           | `~/.opensre/memory/` — what opensre remembers |
| **Never uploaded** | integration credentials and model API keys    |

Credentials stay on the machine. On a second laptop you run the integration
setup again; everything else is already there.

## Turn it on

### Setup command (recommended)

Writes `remote_sync` into `~/.opensre/config.yml`. Ambient credentials stay in
the environment — opensre never stores them.

```bash theme={null}
# Interactive
opensre remote-sync setup

# Non-interactive — AWS/S3
opensre remote-sync setup --provider aws --bucket my-opensre-bucket

# Non-interactive — Google Cloud Storage (uses Application Default Credentials)
opensre remote-sync setup --provider gcs --bucket my-opensre-bucket

# Non-interactive — Vercel Blob (then set BLOB_READ_WRITE_TOKEN in the env)
opensre remote-sync setup --provider vercel --bucket opensre-remote-sync
export BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...

# Non-interactive — Azure Blob (uses ambient credentials via Azure CLI or env vars)
opensre remote-sync setup --provider azure --profile my-storage-account --bucket my-container

# Same from the shell (flags required; no interactive prompts on chat gateways)
# /remote-sync setup --provider vercel --bucket opensre-remote-sync
```

### Environment variables

```bash theme={null}
# AWS/S3 (default)
export OPENSRE_REMOTE_SYNC=1
export OPENSRE_REMOTE_SYNC_BUCKET=my-opensre-bucket

# Or Google Cloud Storage
# export OPENSRE_REMOTE_SYNC=1
# export OPENSRE_REMOTE_SYNC_PROVIDER=gcs
# export OPENSRE_REMOTE_SYNC_BUCKET=my-opensre-bucket

# Or Vercel Blob
# export OPENSRE_REMOTE_SYNC=1
# export OPENSRE_REMOTE_SYNC_PROVIDER=vercel
# export OPENSRE_REMOTE_SYNC_BUCKET=opensre-remote-sync
# export BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...

# Or Azure Blob
# export OPENSRE_REMOTE_SYNC=1
# export OPENSRE_REMOTE_SYNC_PROVIDER=azure
# export OPENSRE_REMOTE_SYNC_PROFILE=my-storage-account
# export OPENSRE_REMOTE_SYNC_BUCKET=my-container
```

| Variable                          | Description                                                                                                         |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `OPENSRE_REMOTE_SYNC`             | Set to `1` to enable. Nothing uploads until you do                                                                  |
| `OPENSRE_REMOTE_SYNC_PROVIDER`    | Backend name. Default `aws` (S3). Built-in: `aws`, `gcs`, `vercel`. Community backends register under the same name |
| `OPENSRE_REMOTE_SYNC_BUCKET`      | Store you own (S3 bucket name, GCS bucket name, or Vercel Blob store name/id). Required                             |
| `OPENSRE_REMOTE_SYNC_PREFIX`      | Key prefix, default `opensre`. Machines that should share history must use the same one                             |
| `OPENSRE_REMOTE_SYNC_REGION`      | Region override when the provider supports it (AWS)                                                                 |
| `OPENSRE_REMOTE_SYNC_PROFILE`     | Named credentials profile, if opensre should not use your default (AWS)                                             |
| `BLOB_READ_WRITE_TOKEN`           | Vercel Blob read-write token (when `provider=vercel`). Ambient; opensre never stores it                             |
| `OPENSRE_REMOTE_SYNC_EXCLUDE`     | Comma-separated glob patterns to keep off the store. See [Exclude paths](#exclude-paths)                            |
| `OPENSRE_REMOTE_SYNC_EXCLUDE_OFF` | Set to `1` to ignore your exclusions for one run and sync everything                                                |

Naming a bucket is not enough on its own — `OPENSRE_REMOTE_SYNC` has to be set
too, so a bucket variable left over from another tool never starts uploading.

`--region` are AWS-only, while --profile is used by AWS and Azure — `opensre remote-sync setup`
rejects either one for a provider that doesn't use it, rather than saving it
silently and never reading it back. (Their env-var equivalents,
`OPENSRE_REMOTE_SYNC_REGION`/`PROFILE`, are read the same way regardless of
provider and are simply ignored by a provider that has no use for them.)

You can also persist settings under `remote_sync` in `~/.opensre/config.yml`
(`enabled`, `bucket`, `provider`, `prefix`, …) — that is what `remote-sync setup`
writes. Credentials are never written there; they stay in your cloud CLI's usual
places. Environment variables still win for a single run.

<Note>
  Every machine that should share your history needs the **same provider, store,
  and prefix** (and for Vercel, a token for that store). Change the prefix only
  when you want a separate, isolated set of conversations — a different prefix is
  a different history, not a backup of the same one.
</Note>

All surfaces share one remote-sync engine entry point:

| Surface                                  | How you invoke it                                             |
| ---------------------------------------- | ------------------------------------------------------------- |
| CLI                                      | `opensre remote-sync status` / `sync` / `setup`               |
| Interactive shell                        | `/remote-sync status` / `sync` / `setup`                      |
| Gateway (Telegram, and any unbound chat) | same slash command (`setup` needs flags; use CLI for prompts) |

<Warning>
  This mirrors a **personal machine**. Turns that belong to an organization —
  Slack and Discord, which bind an org principal — are refused, because object
  keys carry no organization or member id and every member would otherwise share
  the same keys. Organization history already persists through the mounted
  context root (`OPENSRE_CONTEXT_ROOT`); it does not need this.
</Warning>

Check what would move before moving it:

```bash theme={null}
opensre remote-sync status
# or, in the REPL / a chat that supports slash commands:
# /remote-sync status
```

Then sync:

```bash theme={null}
opensre remote-sync sync
opensre remote-sync sync --pull-only    # new machine, fetch only
opensre remote-sync sync --push-only    # back up without pulling
```

On a terminal, a progress bar (`Pulling`/`Pushing`, a count of how many files
are done, and the current file) shows a large `sessions/` tree moving instead
of looking hung; piped or scripted output stays to the final summary line
only. The interactive shell shows the same thing as a live-updating bar.

## Exclude paths

By default every session and every memory file mirrors. Patterns hold some of
them back:

```bash theme={null}
export OPENSRE_REMOTE_SYNC_EXCLUDE='*.tmp,sessions/scratch-*'
```

Or, to keep it between machines, in `~/.opensre/config.yml`:

```yaml theme={null}
remote_sync:
  enabled: true
  bucket: my-opensre-bucket
  exclude:
    - "*.tmp"
    - sessions/scratch-*
    - memory/local-only
```

When the environment variable holds patterns, it replaces the stored list for
that run rather than adding to it. To sync everything once, including the paths
you normally hold back:

```bash theme={null}
OPENSRE_REMOTE_SYNC_EXCLUDE_OFF=1 opensre remote-sync sync
```

Blanking `OPENSRE_REMOTE_SYNC_EXCLUDE` does **not** do this — an empty value
counts as not setting it at all, and your stored list stays in force. That is
deliberate: a script expanding a name that turns out to be unset should not
quietly upload what you meant to keep local. Switching exclusions off is a
separate variable so it has to be done on purpose.

Patterns match the key inside the store — `sessions/…` or `memory/…`, always
with forward slashes — and are case-sensitive, so one settings file means the
same thing on every machine.

| Pattern              | Holds back                                            |
| -------------------- | ----------------------------------------------------- |
| `*.tmp`              | any file with that name, in either root, at any depth |
| `sessions/scratch-*` | sessions whose name starts with `scratch-`            |
| `sessions/archive`   | everything inside that directory                      |
| `memory`             | the whole memory root                                 |

`*` crosses directory separators, so `memory/*` covers nested files too.

Check your patterns took effect before trusting them — `status` shows how many
files under each root are currently held back, so a root you expected to shrink
still showing no count means nothing matched:

```
sessions   /home/you/.opensre/sessions (exists, 4 excluded)
memory     /home/you/.opensre/memory (exists)
Excluded by your settings:
  *.tmp
  sessions/scratch-*
```

An excluded path is held back in **both** directions: not uploaded from this
machine, and not downloaded onto it if another machine already put it there.

<Note>
  Exclusions only ever remove things from the sync. There is no `!pattern` syntax
  to add something back — opensre rejects a pattern starting with `!` — so no
  setting you write can send credentials to the store. Anything already uploaded
  before you added a pattern stays there; sync never deletes. Remove it in the
  store yourself if you want it gone.
</Note>

## Providers

The sync engine talks only to a small object-store interface. Built-in
backends today:

| Provider             | `OPENSRE_REMOTE_SYNC_PROVIDER` | Credentials                                                               |
| -------------------- | ------------------------------ | ------------------------------------------------------------------------- |
| Amazon S3 (default)  | `aws`                          | Ambient AWS config (`AWS_*`, profile, SSO)                                |
| Google Cloud Storage | `gcs`                          | Application Default Credentials (`gcloud auth application-default login`) |
| Vercel Blob          | `vercel`                       | Ambient `BLOB_READ_WRITE_TOKEN`                                           |
| Azure Blob Storage   | `azure`                        | Ambient Azure config (`AZURE_*`, Azure CLI, Managed Identity)             |

Other clouds (GCS, …) are community additions: implement `ObjectStore`,
call `register_object_store("<name>", factory)`, and set
`OPENSRE_REMOTE_SYNC_PROVIDER` — the engine, CLI, and REPL do not change.

### AWS / S3 (default)

Any private bucket works. Uploads request server-side encryption, so a bucket
that requires encryption accepts them unchanged.

Minimum permissions on the prefix you use:

```
s3:ListBucket, s3:GetObject, s3:PutObject
```

`s3:DeleteObject` is not needed — sync never deletes anything. Add
`s3:GetBucketPolicyStatus` (bucket-level, not prefix-scoped) so `status` can
warn you if the bucket is publicly readable — see [The store](#the-store).

### Google Cloud Storage

Set `OPENSRE_REMOTE_SYNC_PROVIDER=gcs` and point `OPENSRE_REMOTE_SYNC_BUCKET`
at any private bucket. Credentials come from Application Default Credentials —
run this once per machine and sign in with the account that owns the bucket:

```bash theme={null}
gcloud auth application-default login
```

Minimum permissions on the bucket (a bucket-scoped `roles/storage.objectUser`
binding covers all four):

```
storage.objects.list, storage.objects.get, storage.objects.create, storage.objects.delete
```

opensre never deletes anything, but GCS still requires `storage.objects.delete`
to **replace** an existing object — without it the first changed session or
memory file fails with a 403. Objects are encrypted at rest by default; there
is nothing to configure.

Add `storage.buckets.getIamPolicy` (bucket-level) so `status` can warn you if
the bucket is publicly readable — see [The store](#the-store). Without it,
`status` shows "could not confirm it is private" instead of failing.

### Vercel Blob

Use a **private** Blob store. Objects are written private (not public URLs).
`OPENSRE_REMOTE_SYNC_BUCKET` is the store name or id for status output; auth is
the read-write token, not the bucket string.

```bash theme={null}
export OPENSRE_REMOTE_SYNC=1
export OPENSRE_REMOTE_SYNC_PROVIDER=vercel
export OPENSRE_REMOTE_SYNC_BUCKET=opensre-remote-sync
export BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...
```

Create a private store and token with the Vercel dashboard or CLI
(`vercel blob create-store my-store --access private`, then connect the store
to a project so `BLOB_READ_WRITE_TOKEN` is available — or paste the token from
the store’s settings).

<Note>
  `BLOB_READ_WRITE_TOKEN` is scoped to one store. Point every machine that should
  share history at the same token (or an equivalent token for that store), plus
  the same prefix.
</Note>

Access mode (private or public) is set once, for the whole store, when you
create it — Vercel does not let you change it afterward. `status` can check
this and warn you if the store came out public, but it needs a **different,
broader** credential than `BLOB_READ_WRITE_TOKEN`: a Vercel account/team API
token, since the store-management API does not accept a Blob read-write
token at all. If you already use the `vercel` integration for observability,
this is the same `VERCEL_API_TOKEN` (and optional `VERCEL_TEAM_ID` for a
token spanning several teams) — nothing new to set up. Entirely optional:
without it, `status` shows "could not confirm it is private" instead of
failing.

```bash theme={null}
# Optional — enables the public-store warning on status
export VERCEL_API_TOKEN=...
# export VERCEL_TEAM_ID=...   # only if the token spans multiple teams
```

### Azure Blob Storage

Any private Azure Storage container works. `OPENSRE_REMOTE_SYNC_BUCKET` is your Container name, and `OPENSRE_REMOTE_SYNC_PROFILE` maps to your Storage Account Name.

Authentication relies entirely on Azure's ambient credentials (`DefaultAzureCredential`). This means you can authenticate via the Azure CLI (`az login`), Managed Identities, or environment variables (`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET`). opensre will never store these credentials directly.

The authenticated principal must have the **Storage Blob Data Contributor** role assignment on the storage account. The generic "Contributor" role is insufficient as it only grants control-plane access, not data-plane access.

## The store

<Warning>
  Keep the store private. It holds your incident conversations and everything
  opensre has remembered about your systems. Prefer a private S3 bucket or a
  private Vercel Blob store — never a public one.
</Warning>

`status` checks this for you on every built-in provider: the bucket policy
(S3), the bucket IAM policy (GCS), or the store's access mode (Vercel Blob,
via the optional `VERCEL_API_TOKEN` above) — if any of them say public,
`opensre remote-sync status` / `/remote-sync status` opens with a warning
instead of silently mirroring your history into it. Without the permission
or credential the check needs, `status` shows "could not confirm it is
private" instead of failing; it never blocks the rest of the command.

## How conflicts resolve

Sync pulls first, then pushes. A file that exists on only one side is copied to
the other. When both sides changed the same file, the more recently written one
wins.

Nothing is ever deleted, on either side. A machine that has been offline for a
month cannot erase work done elsewhere — worst case it re-uploads an old copy
under the same name, and the newer one wins on the next sync.

Deleting a session or a memory on one machine does **not** remove it from the
store or from your other machines. Delete it in the store too if you want it
gone everywhere.
