# Slack MCP in Harbor

Connect your Harbor agent to Slack MCP. Get the licensed image, export a task, verify a reply and reset, then attach your own policy and grader, or run the reference harness.

Run your agent against a [seeded](https://www.rystic.ai/docs/concepts/glossary.md#seed) Slack workspace inside a Harbor sandbox. The image contains the [simulator](https://www.rystic.ai/docs/concepts/glossary.md#simulator), the native Slack MCP server and the episode runtime. Your model inference stays on the host; Slack traffic stays inside the sandbox.

**Start with the setup check below.** It discovers the pinned MCP tools, posts one synthetic reply, reads it back, then proves a reset removes it. It requires no model key. A pinned integration checkout exports the sample task on the trusted host; the episode itself runs from released binaries. Once that passes, [connect your Harbor runner](#connect-your-harbor-runner).

Already using Rystic's `twin-envs` reference harness? Use the [reference run](#reference-run) after obtaining the same image access.

## Before you start

| Requirement | What you need |
|---|---|
| Host | `linux/amd64`, with Docker, Python 3.12 or newer, `uv`, Git and `curl` |
| License | The JSON license supplied by Rystic, covering Slack and Stripe for this sample. Harbor runs on the Enterprise plan's [license file](https://www.rystic.ai/docs/licensing.md#enterprise-license-files), not an API key |
| Image | A customer image reference pinned as `ghcr.io/rystic-inc/twin-envs-slack@sha256:…` |
| Registry access | A GitHub identity granted access to that package and a token with `read:packages` |
| Sample task files | GitHub read access to `Rystic-Inc/twin-envs`, the integration and task repository |
| Model credentials | None for setup checks; your existing host-side provider credentials for a policy rollout |

[Request Harbor access](mailto:founders@rystic.ai?subject=Slack%20Harbor%20access) with your GitHub identity and the products on your license. Request the matching image digest, GHCR package access and read access to `twin-envs`. A repository 404 while signed in means that account has not been granted access. Harbor images contain license-specific binaries: use the image supplied for your license. The ordinary `registry.rystic.ai` product login does not grant access to this GHCR package or repository.

The sample uses Slack for conversations and Stripe HTTP for payment status. No live Slack or Stripe credentials are needed. Pull all dependencies before starting an offline episode. Emulation on another architecture is not part of the measured setup.

## Pull the image

Run on the **trusted Docker host**, in a new working directory. Put your license at `./rystic-license.json`. Set `MCP_IMAGE` to the complete image reference Rystic supplied, and load `GHCR_USER` and `GHCR_TOKEN` from your secret manager.

```bash
: "${MCP_IMAGE:?Set the customer image reference supplied by Rystic}"
: "${GHCR_USER:?Set the GitHub identity granted package access}"
: "${GHCR_TOKEN:?Load a token with read:packages from your secret manager}"
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
docker pull --platform linux/amd64 "$MCP_IMAGE"
unset GHCR_TOKEN
```

Expect a successful pull with a SHA-256 digest. An unauthorized pull is an access problem: confirm the package grant and any organization SSO authorization before continuing. Do not substitute a similarly named product image.

## Export a Harbor task

Generate task files on the **trusted host** using the pinned integration checkout. The image intentionally excludes task splits and grading references; they must be staged separately. This step writes task files without starting an episode or calling a model. It requires the repository access above, but no `rystic-sim` source or Go compiler.

```bash
mkdir -p tasks
git clone https://github.com/Rystic-Inc/twin-envs.git
git -C twin-envs checkout --detach 2d6d6712bd0d314d00aa7bc82a1e0a21e3a942d6
(cd twin-envs && uv sync --frozen && uv run --frozen python -m twinenv.harbor slack \
  --split heldout --mcp --image "$MCP_IMAGE" --out ../tasks)
export TASK_DIR="$PWD/tasks/slack-heldout-mcp/slack_ops-001"
test -f "$TASK_DIR/task.toml"
```

Use `slack_ops-001` for the initial check. Its task package contains:

| File | Who receives it |
|---|---|
| `task.toml` | Trusted runner: image, [scenario](https://www.rystic.ai/docs/concepts/glossary.md#scenario), environment, bootstrap and user settings |
| `instruction.md` | Agent: the task prompt |
| `tests/task.json` and `tests/test.sh` | Verifier only: task definition and grading entry point |
| `solution/` | Reference solver only; keep it out of the policy's workspace |

Keep these files together. The generated task name is qualified, such as `rystic/slack-heldout-slack_ops-001`; use the exact name in `task.toml` when matching results.

## Start a disposable setup episode

This section shows the Docker operations your Harbor runner must perform. All shell commands run on the **trusted host**; each `docker exec --user …` selects the identity inside the container.

Prepare a private environment file from the generated task. This resolves the license without placing it in command arguments. It does not add model credentials to the sandbox. Save this script as `episode_env.py`:

```python
import os
from pathlib import Path
import tomllib

task = tomllib.loads((Path(os.environ["TASK_DIR"]) / "task.toml").read_text())
values = dict(task["environment"]["env"])
values["RYSTIC_LICENSE_B64"] = os.environ["RYSTIC_LICENSE_B64"]
values["TWINENV_SESSION_SECONDS"] = ""
if any("${" in value or "\n" in value or "\r" in value for value in values.values()):
    raise SystemExit("Unresolved or multiline task environment value")
fd = os.open("episode.env", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as out:
    out.write("".join(f"{key}={value}\n" for key, value in values.items()))
```

Then write `episode.env` and start the container:

```bash
export RYSTIC_LICENSE_B64="$(base64 < ./rystic-license.json | tr -d '\n')"
python3 episode_env.py
unset RYSTIC_LICENSE_B64

export EPISODE="rystic-slack-setup-$(date +%s)"
docker run --detach --pull never --platform linux/amd64 \
  --name "$EPISODE" --network none --security-opt no-new-privileges \
  --cap-drop NET_ADMIN --cap-drop NET_RAW --cpus 1 --memory 2048m \
  --env-file episode.env --workdir /app --entrypoint sleep "$MCP_IMAGE" infinity
rm episode.env
docker cp "$TASK_DIR/tests/." "$EPISODE:/tests/"
docker exec --user root "$EPISODE" chown -R root:twinenv /tests
docker exec --user root "$EPISODE" chmod 0750 /tests
docker exec --user root "$EPISODE" python -P -m twinenv.sandbox up
```

Expect bootstrap to complete and create `/run/twinenv/mcp.json` with `ready: true`. Allow up to five minutes for initial bootstrap. Stop on a nonzero exit; do not give the agent an unready sandbox. If the setup is interrupted, [clean up](#cleanup) before starting a new disposable episode.

The container starts as root so bootstrap can install local routing and drop service privileges. Policy commands must run as `agent`; verifier commands run as `twinenv`. Keep `/tests`, `/var/lib/twinenv`, `/rystic` and the Docker socket inaccessible to the policy. This image inherits the license through its environment; run it only on trusted hosts with sandbox egress disabled.

## Check tools, a reply and reset

Download the [setup check](https://www.rystic.ai/examples/slack-harbor/mcp-smoke.py) and [expected pins](https://www.rystic.ai/examples/slack-harbor/pins.json) on the host, then copy them into a trusted directory in the disposable container:

```bash
mkdir -p checks
curl -fsS https://www.rystic.ai/examples/slack-harbor/mcp-smoke.py -o checks/mcp-smoke.py
curl -fsS https://www.rystic.ai/examples/slack-harbor/pins.json -o checks/pins.json
docker exec --user root "$EPISODE" mkdir -p /opt/rystic-setup
docker cp checks/. "$EPISODE:/opt/rystic-setup/"
docker exec --user root "$EPISODE" chown -R root:root /opt/rystic-setup
docker exec --user root "$EPISODE" chmod -R a-w /opt/rystic-setup
docker exec --user agent "$EPISODE" \
  python -I /opt/rystic-setup/mcp-smoke.py write
```

Expect JSON containing `"ok": true`, `"phase": "write"`, a generation ID and the selected tool names. The check verifies the protected manifest, discovers the tools, reads a thread, writes exactly one uniquely marked reply and reads it back. It never retries a write. Run it only in this disposable setup episode, before attaching a policy or evaluating rewards.

Reset as the trusted controller, then open a new client session to check the original thread:

```bash
docker exec --user root "$EPISODE" python -P -m twinenv.sandbox reset
docker exec --user agent "$EPISODE" \
  python -I /opt/rystic-setup/mcp-smoke.py after-reset
```

Expect `"ok": true`, `"phase": "after-reset"` and a different generation ID. The original thread contents must match and the setup reply must be gone. Keep the small JSON results as your integration check. These checks establish connectivity and a clean reset, not a task reward or a training result.

## Connect your Harbor runner

Use the exported task as the contract for your existing runner. Harbor integrations differ; verify that your loader honors each setting rather than assuming that accepting a `task.toml` file applies all of it. The [reference harness](#reference-run) pins the loader implementation used in the measured run.

| Runner responsibility | Required behavior |
|---|---|
| Create the environment | Use `environment.docker_image`, resolve `environment.env`, disable external networking, and apply CPU/memory limits |
| Stage protected files | Keep the grader under `/tests`, owned by `root:twinenv`, with directory mode `0750`; stage the prompt separately |
| Bootstrap | Run `environment.healthcheck.command` as root and wait for success |
| Run the policy | Honor `agent.user = "agent"`; keep model inference and its API key on the host |
| Attach MCP | Read the protected manifest and connect from inside this container's network namespace |
| Grade | Run `tests/test.sh` as `verifier.user = "twinenv"`; read `/logs/verifier/reward.txt` from the trusted host |
| Finish | Close the MCP client and destroy the container, or perform the full reset below |

The agent-readable `/run/twinenv/mcp.json` contains `url`, `generation`, `ready`, `profile_id`, `profile_sha256`, `tool_manifest_sha256` and `native_binary_sha256`. Validate the profile and tool hashes against the [expected pins](https://www.rystic.ai/examples/slack-harbor/pins.json); reject a missing, writable, mismatched or unready manifest.

The expected identity fields are:

```json
{
  "profile_id": "slack-korotovsky-v1.3.0-bot-static-v1",
  "profile_sha256": "34fbf7a35ec57e24d5f9808660b2087eb10237dbc3a45710402a6ebf28c097ea",
  "tool_manifest_sha256": "af7a723aee090c905f6128941b36cf3f6fce243197650d7484e05dab79b4d39a"
}
```

Connect an MCP client to that `url`, initialize a session and call `tools/list`. Pass the returned tool definitions unchanged to your policy. Keep the client inside the sandbox: a loopback URL read from the container points at the wrong machine if used by a host-side process. Use your runner's in-container MCP bridge, or run the SDK client as `agent` through container exec. The downloadable setup check demonstrates this connection using the SDK already in the image.

Do not launch a second native server, add a live Slack token, expose the local gateway on a public port, or route it to the website's docs MCP endpoint. The [Slack simulator's docs](https://www.rystic.ai/docs/simulators/slack.md) cover the MCP server, its gateway and its tools.

For the sample task, Stripe stays on its existing HTTP interface. Preserve the exported prompt and grader when comparing with the reference result. For your own task, stage your prompt and protected grader and seed the scenario through trusted bootstrap; do not give the policy control-plane credentials or grader files.

Run the original sample grader from the trusted host after the policy has finished:

```bash
docker exec --user twinenv "$EPISODE" sh /tests/test.sh
docker exec --user root "$EPISODE" cat /logs/verifier/reward.txt
```

The [reference run](#read-the-result) earns `1.0`; your policy's reward depends on its actions. Grading an untouched environment or the setup check is not expected to earn `1.0`. The grader evaluates before/after behavior through public APIs and the protected call record.

## Reset between episodes

Close the old MCP session before running the trusted `python -P -m twinenv.sandbox reset`. Wait for bootstrap to finish, read the new `/run/twinenv/mcp.json`, verify its changed generation and pins, then initialize a new session. Reset replaces simulator state, native process/cache state, credentials and the local CA. Never reuse the previous connection receipt or MCP session.

`POST /_rystic/reset` alone only resets a simulator's state. It is insufficient for an MCP episode. A timeout, cancellation or native-process failure withdraws readiness; stop policy calls and perform the full reset before resuming.

Keep one mutable sandbox per concurrent episode. Share immutable image layers, but do not share state directories, credentials, manifests or MCP sessions. Lifecycle isolation has been measured with two concurrent episodes; choose larger worker counts after measuring your own resource limits. The reference runtime's storage declaration does not enforce a disk quota; configure that in your container infrastructure if needed.

## Cleanup

After inspecting the setup results, destroy the disposable container. Use a fresh one for the actual policy rollout.

```bash
docker exec --user root "$EPISODE" python -P -m twinenv.sandbox down
docker rm --force "$EPISODE"
rm -f episode.env
```

If bootstrap failed and `down` cannot complete, remove the disposable container of that name. Keep the license file in your secret store and exclude licenses, `episode.env` and policy traces from source control.

## Reference run

This recipe runs Rystic's reference harness on the same task and image contract instead of your own runner. Complete [Pull the image](#pull-the-image) first. The checkout's lockfile pins the Rystic `verifiers` fork that honors distinct agent and verifier users, so keep the lockfile and use `--frozen`.

### Stage the task

Run these commands on the trusted host, in a directory without an earlier `twin-envs` clone. Keep `MCP_IMAGE` set to the image supplied for your license.

```bash
git clone https://github.com/Rystic-Inc/twin-envs.git
cd twin-envs
git checkout --detach 2d6d6712bd0d314d00aa7bc82a1e0a21e3a942d6
uv sync --frozen
uv run --frozen python scripts/harbor_stage.py slack-heldout --mcp --image "$MCP_IMAGE"
```

Expect the dataset name `rystic/slack-heldout-mcp` and its local Harbor cache path. The generated files are also under `harbor/tasks/slack-heldout-mcp/slack_ops-001`. The checkout pins the task, grader, adapter and dependency lock together.

### Run a policy

Load `RYSTIC_LICENSE_B64` and `ANTHROPIC_API_KEY` into the host environment from your secret manager. `RYSTIC_LICENSE_B64` is the base64-encoded license JSON. The provider key stays with host-side inference; never put it in `task.toml`, the agent prompt or the container environment.

The supplied configuration selects `claude-opus-5`. A rollout makes billable calls to that provider. The dry run checks configuration without running the policy:

```bash
: "${RYSTIC_LICENSE_B64:?Load your base64-encoded license}"
: "${ANTHROPIC_API_KEY:?Load your host-side inference key}"
uv run --frozen eval @ configs/slack-heldout-harbor-mcp-hosted.toml --dry-run --no-rich
uv run --frozen eval @ configs/slack-heldout-harbor-mcp-hosted.toml \
  -o outputs/mcp-qualification --run.dir agent --no-rich
```

The configuration selects the Harbor Docker runtime, one rollout, the original `slack_ops-001` task and the `twinenv_mcp_harness` adapter. That adapter validates the protected manifest and adds the container-local MCP connection to the existing policy loop. Stripe keeps its HTTP interface.

### Read the result

Inspect the reward and trace under `outputs/mcp-qualification/agent`. A successful process exit alone does not establish a successful task. Check the exact staged task name, completed rollout, numeric reward and acknowledged tool calls.

The measured reference rollout earns `1.0` with 17 MCP replies, zero extra mutations and zero forbidden calls. Its Slack work uses the pinned MCP tools, and its Stripe calls use HTTP. This is a reference outcome, not a guarantee for every policy or sampling run.

For the full provider-free lifecycle and failure checks, run the existing checker against your image before a large batch:

```bash
uv run --frozen python scripts/check_slack_mcp_episodes.py \
  --image "$MCP_IMAGE" \
  --task-dir harbor/tasks/slack-heldout-mcp/slack_ops-001 \
  --release-manifest configs/slack-mcp-release-linux-amd64.json \
  --repeat 3 --out outputs/mcp-qualification/episodes
```

It must exit zero and produce `outputs/mcp-qualification/episodes/summary.json`. It exercises two episodes, deterministic reset and failure recovery without model inference. Keep receipts private until they have been checked for credentials and customer information, and keep the [supported configuration](#supported-configuration) with any results you share.

## Supported configuration

| Component | Measured configuration |
|---|---|
| Slack / Stripe | `0.0.6` / `0.0.4` |
| Native MCP server | korotovsky/slack-mcp-server `1.3.0`, synthetic bot |
| Platform | `linux/amd64` |
| Profile | `slack-korotovsky-v1.3.0-bot-static-v1` |
| Tools | `channels_list, conversations_history, conversations_replies, conversations_add_message` |
| Scenarios | Static; numeric history/reply limits |

The measured reference image is `ghcr.io/rystic-inc/twin-envs-slack@sha256:bb6f9284ef7f2ac84023a10f51118fa3317beff6e01905977ce6f3aeff41400c`. Your license-specific image may have a different digest; the supplied profile and tool hashes must match this contract. The downloadable pins record the reference image, versions and hashes together so a later standalone release cannot silently change this setup.

The packaged workflow passes 13 declared cases, and the held-out policy run earns full task reward. These are scoped checks, not a general Streamable HTTP certification. The [Slack simulator's docs](https://www.rystic.ai/docs/simulators/slack.md) cover what the MCP tools leave out, and its [coverage](https://www.rystic.ai/docs/simulators/slack.md#coverage) is the separate HTTP API surface.

## Troubleshooting

| Symptom | Check |
|---|---|
| Image pull is unauthorized | Package access, `read:packages`, organization SSO, and the image reference supplied with your license |
| License or product mismatch | Use the customer-matched image and a license covering both Slack and Stripe |
| `exec format error` | Use a `linux/amd64` worker and the supplied platform image |
| Protected sandbox or user error | Start bootstrap as root; policy as `agent`; verifier as `twinenv`; preserve the image's users |
| Missing or unready MCP manifest | Inspect bootstrap's exit status; fix it before starting the policy |
| MCP connection refused from the host | Connect inside the container's network namespace |
| Profile/tool mismatch | Use matching image and pins; do not bypass the check or auto-upgrade the native server |
| A request times out or reset changes generation | Close the client, reset through the trusted controller, read the new manifest, reconnect |
| Grader cannot read its task | Stage the complete `tests/` directory and preserve `root:twinenv` ownership and directory permissions |

For an agent reading this guide: run commands in order, check each expected result, and stop on a failed prerequisite. Do not replace pinned artifacts with `latest`, weaken permissions, retry an uncertain mutation, or infer that a running HTTP endpoint proves MCP is ready.

---

Source: https://www.rystic.ai/docs/slack-harbor · Markdown: https://www.rystic.ai/docs/slack-harbor.md
