Guides
Slack MCP in Harbor
Run your agent against a seeded Slack workspace inside a Harbor sandbox. The image contains the 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.
Already using Rystic’s twin-envs reference harness? Use the 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, 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 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.
: "${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.
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, 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:
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:
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 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 and expected pins on the host, then copy them into a trusted directory in the disposable container:
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:
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 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; reject a missing, writable, mismatched or unready manifest.
The expected identity fields are:
{
"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 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:
docker exec --user twinenv "$EPISODE" sh /tests/test.sh
docker exec --user root "$EPISODE" cat /logs/verifier/reward.txt
The reference run 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.
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 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.
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:
: "${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:
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 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 cover what the MCP tools leave out, and its 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.