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 four 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, Docker, Python 3.12 or newer, uv, Git and curl |
| License | The JSON license supplied by Rystic, covering Slack and Stripe for this sample |
| 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. The measured setup is Linux amd64; emulation on an arm64 laptop is not part of that measurement.
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
(
set -e
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, world, 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.
export RYSTIC_LICENSE_B64="$(base64 < ./rystic-license.json | tr -d '\n')"
python3 - <<'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()))
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 four 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.
The local gateway accepts Streamable HTTP; its upstream Slack MCP process uses stdio. Do not launch a second native server, add a live Slack token, expose the gateway on a public port, or route it to the website’s docs MCP endpoint.
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 world 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 measured reference policy earns 1.0 with 17 acknowledged MCP replies and no extra mutations. Your policy’s reward depends on its actions. Grading an untouched world 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 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 world 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 that same named disposable container. Keep the license file in your secret store and exclude licenses, episode.env and policy traces from source control.
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, stdio |
| Platform | Linux amd64 |
| Profile | slack-korotovsky-v1.3.0-bot-static-v1 |
| Tools | channels_list, conversations_history, conversations_replies, conversations_add_message |
| Worlds | Static scenarios; 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. Hosted Slack MCP/OAuth, search, uploads, reactions, browser credentials, MCP resources and general Streamable HTTP certification are outside this contract. See Slack HTTP coverage for the separate 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.