A personal Hermes agent that surfaces what the developer community is working on, struggling with, asking about, and flagging as gaps — and compares it against what internal developer/product teams are prioritizing, so resources can be aligned against actual community demand. The agent draws on signal from live GitHub repository state, mirrored GitHub discussions, NVIDIA forums, and Slack channels; you interact with it via Outlook email and/or Slack. Outlook is the recommended primary channel, but either is enough on its own — at least one of the two must be configured.
This is a personal agent designed to run on a managed image/VM provisioned by enterprise IT (e.g. Ubuntu) — one you authenticate into, that ships sanctioned pre-installed software and can reach only specific resources. The agent rides that infrastructure with delegated access: its credentials live in OpenShell providers (GitHub, Slack, Microsoft Graph), and it acts on your behalf within them.
Protection is layered. The managed image provides coarse protections (authenticated access, a restricted set of reachable resources, host hardening). OpenShell adds workload-specific protections on top: per-sandbox L7 egress allowlists, credential placeholder substitution (real secrets never enter the sandbox), binary allowlists, and Landlock/seccomp.
Concretely, the OpenShell gateway runs on that host under its Docker compute driver:
each sandbox is a host-networked container, and the agent reaches the example's host-side
services (the PostgREST forum bridge, ATIF export relay) through
host.openshell.internal. Under this driver the container is a thin, host-networked
execution layer — the meaningful workload boundary is OpenShell's policy/proxy/credential
and Landlock/seccomp enforcement, not the container itself. This is a single-host model,
not a Kubernetes/cluster deployment.
The Hermes sandbox operates with a deliberately narrow egress policy. It connects
live to Slack and Outlook for interactions and research. It also has
authenticated, read-only GitHub REST access to one configured repository
(GITHUB_READONLY_REPO). The GitHub token is attached through an OpenShell
provider placeholder so GitHub rate limits are practical, while policy.yaml
still limits the sandbox to repo-scoped GET requests. GitHub discussions,
historical mirror data, and NVIDIA forum data come from host-side ETL
containers that scrape on a schedule, write results into Postgres, and expose
that mirror through a read-only PostgREST HTTP bridge.
%%{init: {'theme': 'default', 'flowchart': {'nodeSpacing': 50, 'rankSpacing': 100, 'curve': 'basis', 'padding': 20}, 'themeVariables': {'fontSize': '13px'}}}%%
flowchart LR
nvidia["Internal\nLLM Inference Provider"]
entra["Internal\nMS Entra ID"]
slack["Internal\nSlack workspace"]
outlook["Internal\ngraph.microsoft.com\nmailbox"]
github["External\nGitHub API"]
forums["External\nNVIDIA Forums\n(nemoclaw tag)"]
s3["Internal\nAWS S3 (prod)\nATIF trace storage"]
subgraph host["Host Machine/Virtual Machine"]
direction TB
subgraph supervisor["OpenShell Sandbox Supervisor"]
direction TB
l7["OpenShell L7 Proxy\n10.200.0.1:3128"]
privacyRouter["OpenShell Privacy Router\n(CONNECT proxy)"]
subgraph sandbox["OpenShell Sandbox"]
direction LR
agent["Hermes Agent\n+ Slack messaging channel"]
relayRuntime["native NeMo Relay\n(in-process Hermes plugin)"]
outlookBridge["outlook-bridge\nMS Graph poller"]
atifBridge["atif-bridge\n127.0.0.1:18444\nHTTP→HTTPS shim"]
traceDisk[("/tmp/atif/\n(local or recovery)")]
subgraph sourceSkills["Source Skills"]
direction LR
s1["source-etl-query"]
s4["cross-source-gap-analysis"]
end
subgraph slackSkills["Slack Skills"]
direction LR
k1["slack-channel-finder"]
k2["slack-channel-summarizer"]
end
subgraph outlookSkills["Outlook Skills"]
direction LR
o1["outlook-email-search"]
end
outlookBridge <-->|"HTTP POST\ndeliver · reply"| agent
agent -->|"tool call\nskill dispatch"| sourceSkills
agent -->|"tool call\nskill dispatch"| slackSkills
agent -->|"tool call\nskill dispatch"| outlookSkills
agent -->|"in-process\nscope events"| relayRuntime
relayRuntime -.->|"file write\nlocal or recovery"| traceDisk
relayRuntime -->|"HTTP POST /atif\ncompleted trajectory"| atifBridge
relayRuntime -->|"OTLP HTTP POST\ntelemetry traces"| l7
atifBridge -->|"HTTPS POST /atif\nbearer placeholder"| l7
outlookBridge -->|"HTTPS GET/POST\nmail poll · reply"| l7
agent <-->|"WSS socket-mode\nmessaging channel"| l7
agent -->|"HTTPS POST\nLLM request"| privacyRouter
sourceSkills -->|"HTTP GET\nsource queries"| l7
slackSkills -->|"HTTPS POST\nSlack API"| l7
outlookSkills -->|"HTTPS GET\nGraph API"| l7
end
end
gateway["OpenShell Gateway\n127.0.0.1:17670\nprovider store ·\nrefresh-token rotation"]
atifRelay["atif-export-relay\n:18443\nbearer auth · re-signs"]
phoenix["Phoenix\n:6006"]
postgrest["PostgREST\n:3100"]
postgres[("PostgreSQL\nsource mirror")]
etls["Source ETLs\nGitHub + Forums\nhourly deltas"]
minio["MinIO (dev)\n:9000"]
l7 -->|"OTLP HTTP POST\ntrace ingest"| phoenix
l7 -->|"HTTP GET\nREST queries"| postgrest
l7 -->|"HTTPS POST /atif\nBearer substituted"| atifRelay
postgrest -->|"SQL\ndata queries"| postgres
etls -->|"SQL INSERT\nhourly deltas"| postgres
gateway <-->|"gRPC stream\ncredential refresh"| l7
atifRelay -.->|"boto3 PutObject"| minio
end
privacyRouter -->|"HTTPS POST\nLLM inference"| nvidia
l7 <-->|"WSS / HTTPS POST\nSlack messaging"| slack
l7 -->|"HTTPS GET/POST\nGraph API"| outlook
l7 -->|"HTTPS GET\nGitHub REST"| github
atifRelay -->|"boto3 PutObject"| s3
gateway <-->|"HTTPS POST\ntoken rotation"| entra
etls -->|"HTTPS GET\nscheduled scrape"| github
etls -->|"HTTPS GET\nscheduled scrape"| forums
style host fill:#f7f6ef,stroke:#8a8068,stroke-width:2px
style supervisor fill:#e7f0ff,stroke:#2b5fab,stroke-width:3px
style sandbox fill:#d8e8ff,stroke:#2b5fab,stroke-width:1px,stroke-dasharray:5 3
style sourceSkills fill:#f0f4ff,stroke:#7090cc,stroke-width:1px
style slackSkills fill:#f0f4ff,stroke:#7090cc,stroke-width:1px
style outlookSkills fill:#f0f4ff,stroke:#7090cc,stroke-width:1px
style agent fill:#dbeafe,stroke:#3b82f6,stroke-width:1.5px
style relayRuntime fill:#fef9e7,stroke:#f39c12,stroke-width:2px
style atifBridge fill:#fef0e7,stroke:#e67e22,stroke-width:2px
style atifRelay fill:#fef0e7,stroke:#e67e22,stroke-width:2px
style outlookBridge fill:#fef0e7,stroke:#e67e22,stroke-width:2px
style traceDisk fill:#fef9e7,stroke:#f39c12,stroke-width:1px
style l7 fill:#fce5cd,stroke:#e69138,stroke-width:2px
style privacyRouter fill:#e7eef0,stroke:#5d6d75,stroke-width:2px
style gateway fill:#e7eef0,stroke:#5d6d75,stroke-width:2px
style s3 fill:#eef7e9,stroke:#6aa84f,stroke-width:2px
style minio fill:#eef7e9,stroke:#6aa84f,stroke-width:1px,stroke-dasharray:4 2
classDef internal fill:#eef7e9,stroke:#6aa84f,stroke-width:2px
classDef external fill:#fce5cd,stroke:#e69138,stroke-width:2px
class nvidia,slack,outlook,entra,s3 internal
class github,forums external
Key invariants:
- The agent has authenticated read-only
api.github.qkg1.topaccess for exactly one configured repo. The raw GitHub token stays in an OpenShell provider; the sandbox sees only a placeholder, and policy still blocks writes, non-API GitHub hosts,git, andgh. - GitHub discussions, historical mirror data, and NVIDIA forum data come from the Postgres mirror.
- Slack and Outlook are live connections from the sandbox; the agent can read and write both in real time.
- Compatible-endpoint inference egress is required for the agent's LLM calls — it's not a research/data-ingestion path.
- The ETL containers are non-agentic — fixed scraper logic on an hourly interval, no LLM involvement.
- The PostgREST bridge exposes a read-only HTTP API on host port
3100, and the sandbox reaches it throughhost.openshell.internalwithout any live forum egress. - Hermes runs NeMo Relay in process through its native plugin integration, with no separate Relay installation or process.
Skills are loaded on demand by the agent when relevant to a task. They live in agents/hermes/skills/. 0.0.
| Skill | Purpose |
|---|---|
github-readonly-live |
Query the configured live GitHub repo via authenticated, policy-scoped REST GET requests. |
source-etl-query |
Query the host-side PostgREST bridge for mirrored GitHub discussions, historical mirror data, and NVIDIA forum data. |
slack-channel-finder |
Discover Slack channels by topic, team, or domain and infer what each channel is for. |
slack-channel-summarizer |
Resolve Slack channels by name or ID and read message history via the Slack Web API. |
outlook-email-search |
Search the Outlook mailbox via Microsoft Graph to find and read emails relevant to a question. |
cross-source-gap-analysis |
Synthesize findings across Slack, GitHub, and NVIDIA forum sources to identify gaps, alignment issues, and follow-ups. |
nemoclaw-autoheal |
Guide users through sandbox health checks and optional host-side auto-heal setup. |
nemoclaw-nvteam |
Route work through eight evidence-bounded role lenses added locally by this Community recipe. |
The original contribution reported source revision
b87038405fd7d9646dba57c367f54d86ca4d933d. This repository adapts and hardens
that package for the public recipe; it does not claim byte-for-byte identity
with the reported source. The repository commit that contains this example is
the auditable version.
Named-person authority weighting is disabled by default. This public recipe does not provision a private registry or a secure registry-installation workflow. It includes only the schema, synthetic example, and validator for a trusted runtime that supplies the registry read-only outside agent-writable state.
The eight role lenses are Product Manager (River), Technical Program Manager (Quinn), Backend and Systems Engineer (Akira), Data and ML Engineer (Jordan), Quality Engineer (Robin), Platform and SRE (Alex), Security Engineer (Morgan), and Technical Marketing Engineer (Parker). The chief of staff introduces this team once at the start of the first NVTeam-routed response and shows the active name and role on each routed response. NVTeam is added by this Community recipe; it is not a built-in capability of the core NemoClaw product. These labels describe task-scoped lenses, not real people, separate agents, models, configurations, decision owners, or evidence about core-product behavior.
Substantive NVTeam work applies Mission is the Boss, Speed of Light (SOL), Listen, Understand, Answer (LUA), and “As much as needed, as little as possible” as practical decision tools. The personas apply these principles silently by default and name one only when it clarifies a material choice. Akira, Jordan, Robin, Alex, Morgan, and Parker share one technical-writing reference instead of duplicating the same rules across their cards.
In Slack, every persona uses semantic Markdown shaped for its domain. Hermes renders the Markdown as native Rich Blocks and retains a complete text fallback. When two to four choices materially block progress, the normal Hermes clarification tool appears as one-tap Slack buttons. A click returns user input to the normal turn; it does not approve or perform a side effect.
The bring-up has two distinct halves: a host-side bootstrap (Docker services that hold state across sandbox lifecycles) and an agent-side bring-up (the OpenShell sandbox itself). The session UUID for Outlook gets produced between them, so the order matters.
$ git clone https://github.com/NVIDIA/nemoclaw-community.git && cd examples/recipes/nvidia/developer-community-chief-of-staff/
$ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=v0.0.85 shOpenShell v0.0.85 matches the supported version in the NemoClaw v0.0.105
release that publishes this example's pinned Hermes sandbox base image.
The package-managed installer starts a local gateway service for you. This
example assumes that default path and targets the openshell gateway at
https://127.0.0.1:17670. If you're following OpenShell's snap instructions
instead, set OPENSHELL_GATEWAY=snap-docker and
OPENSHELL_GATEWAY_ENDPOINT=http://127.0.0.1:17670 in .env.
On Debian/Ubuntu the installer registers openshell-gateway as a systemd
user service, which only auto-starts when your user has an active systemd
session. Headless hosts (cloud shells, SSH-only VMs, CI runners) often don't,
so the service silently never starts and the first openshell gateway add
call fails with mTLS certificates for gateway 'openshell' were not found.
The fix, per the OpenShell install docs,
is to enable linger so the user manager boots without an interactive login:
$ sudo loginctl enable-linger $USER
$ export XDG_RUNTIME_DIR=/run/user/$(id -u) # only needed in shells started before linger
$ systemctl --user start openshell-gateway # if not already started
$ systemctl --user status openshell-gateway # verifyIf systemctl --user returns Failed to connect to bus: No medium found
even after enable-linger, it's because the current shell predates the
user manager and doesn't know where the bus is. Either export
XDG_RUNTIME_DIR as shown above, or log out and reconnect — pam_systemd
sets it automatically once linger is on.
The service's ExecStartPre provisions the mTLS bundle the CLI needs, so
once the unit is active (running), bring-up.sh can register the gateway.
This example uses OpenShell provider v2 (OAuth refresh-token rotation via
openshell provider refresh configure). Enable it once at gateway scope:
$ openshell settings set --global --key providers_v2_enabled --value true --yesscripts/02-providers.sh verifies this is set and refuses to run otherwise.
You also need a running Docker daemon. If you haven't already, register an Azure
application and a dedicated agent mailbox per docs/set-up-outlook-bridge.md
— that's a one-time setup that produces your OUTLOOK_CLIENT_ID and OUTLOOK_TENANT_ID.
This example will download and install additional third-party open source software projects. Review the license terms of these open source projects before use. The repository-level THIRD-PARTY-NOTICES file tracks the expected inventory.
$ cp .env.example .envNow edit .env and fill in everything you already have:
COMPATIBLE_API_KEY— your inference key- At least one messaging channel — Outlook or Slack (or both):
- Outlook (recommended primary channel): set all four Outlook vars together, or leave the entire block empty.
OUTLOOK_TENANT_ID,OUTLOOK_CLIENT_ID— from your Azure app registrationOUTLOOK_TARGET_MAILBOX,OUTLOOK_REPLY_TO— the agent's mailbox and your personal mailbox
- Slack:
SLACK_BOT_TOKEN/SLACK_APP_TOKEN(both required) — see docs/set-up-slack.md. Partial Outlook configuration (some vars set, some empty) is rejected at bring-up.
- Outlook (recommended primary channel): set all four Outlook vars together, or leave the entire block empty.
- (optional)
SLACK_ALLOWED_IDS— comma-separated Slack user IDs to restrict who can DM the agent; leave empty to allow anyone in the workspace - (optional)
NEMOCLAW_SLACK_RICH_BLOCKS=false— disable native Slack Rich Block rendering and use the text fallback. The default istrue. - (optional)
OUTLOOK_ALLOWED_SENDERS— comma-separated allowlist of email senders the agent will respond to; leave empty to fall back to OUTLOOK_REPLY_TO - (optional)
GITHUB_TOKENfor authenticated sandbox read-only GitHub REST,GITHUB_READONLY_REPO,PHOENIX_COLLECTOR_ENDPOINT,PHOENIX_PROJECT_NAME
scripts/bring-up.sh handles host services as its phase 1/4 by
invoking scripts/00-host-services.sh before the
sandbox-side phases. The stack from extras/docker-compose.yml
— phoenix (telemetry), postgres (ETL backing store), the forum ETL, PostgREST on
host port 3100, plus the opt-in GitHub ETL and minio + atif-export-relay when
configured — is
designed to outlive the sandbox, so subsequent tear-down.sh && bring-up.sh cycles
re-touch only the sandbox by default (00-host-services is idempotent).
You can also manage host services independently: bash scripts/00-host-services.sh up
or bash scripts/00-host-services.sh down for direct control without touching the
sandbox.
$ bash scripts/bring-up.shThe script auto-sources .env, then runs 01-gateway.sh → 02-providers.sh →
03-sandbox.sh (select or register the local OpenShell gateway, import v2 provider
profiles, upsert providers, build and launch the sandbox).
Before the image build, provider setup sends one bounded synthetic tool request
and requires a valid structured tool call. After selecting the route, it also
confirms that OpenShell reports the requested provider and model as active. A
failure stops setup before the expensive build. Set
NEMOCLAW_INFERENCE_PREFLIGHT=0 only as an explicit bypass for intentional
offline setup or an endpoint that cannot support verification.
On the first bring-up with Outlook configured, 02-providers.sh runs an interactive
Microsoft device-code login (it prints a URL + code; complete it in a browser as the
OUTLOOK_TARGET_MAILBOX user) and caches the resulting refresh token at
.bootstrap/cache/ms-graph-token.json (mode 0600; ignored by .gitignore). Subsequent
bring-ups reuse the cached refresh token (auto-refreshing on staleness ~90 days). Force
a fresh login with OUTLOOK_LOGIN_CACHE=2 bash scripts/bring-up.sh. Set
OUTLOOK_LOGIN_CACHE=0 to skip the cache entirely and do device-code on every
bring-up — see docs/set-up-outlook-bridge.md.
To use Hermes interactively after bring-up, connect to the sandbox and start a new TUI session:
$ openshell sandbox connect hermes-direct
$ hermes chat --tuiUse hermes chat --tui --continue only after a TUI session exists. The image
contains the TUI bundle and does not need an npm install at runtime.
Hermes includes NeMo Relay as a normal runtime dependency. In the default local
export mode, a finalized session is written to /tmp/atif/. If
PHOENIX_COLLECTOR_ENDPOINT is set, 03-sandbox.sh additionally bakes the
endpoint into the image so OpenInference traces stream into Phoenix at
http://localhost:6006.
After the sandbox is Ready and the normal verification passes, you can opt in
to user-level monitoring that keeps the host proxy, Hermes gateway forward, and
Slack response path healthy. It is deliberately separate from bring-up.sh so
first-time setup stays predictable and operators retain control over systemd.
$ bash scripts/autoheal/install.sh --check
$ bash scripts/autoheal/install.sh
$ bash scripts/autoheal/sanity-check.shIf you installed auto-heal from the previous
examples/personal-community-sentiment-triage path, run the last two commands
again from this directory. Restore the ignored .env file at this new path
first, and keep it out of Git. The installer updates its absolute paths and
restarts only the units that it owns.
For a host TLS proxy, first set the explicit upstream origin in .env. For
example, NEMOCLAW_ENDPOINT_URL=http://host.openshell.internal:18080/v1 pairs
with NEMOCLAW_HOST_TLS_PROXY_UPSTREAM=https://inference-api.nvidia.com.
See docs/auto-heal.md for manual checks, logs, repair, and
uninstall instructions.
- Owns (in this directory):
agents/hermes/(the full Hermes asset tree, staged here for convenience),policy.yaml(sandbox network/filesystem policy template),extras/,.env, andscripts/:00-host-services.sh— host-side stack lifecycle (phoenix, postgres, ETLs, postgrest, and minio + atif-export-relay whenATIF_EXPORT_MODE=relay). Idempotent; safe to invoke directly forupordown.01-gateway.sh/02-providers.sh/03-sandbox.sh— sandbox-side phase scripts called by the bring-up orchestrator.bring-up.sh— orchestrator: invokes00-host-services.sh up(phase 1/4) followed by 01 → 02 → 03 (phases 2-4/4).00-host-services.shis idempotent, so re-runningbring-up.shon an already-up host stack is a no-op for those services and only re-runs the sandbox-side phases.tear-down.sh— removes the sandbox and per-sandbox providers; preserves host services. Add--stop-host-servicesto also stop them (volumes preserved) or--purge-host-servicesto stop and wipe named volumes.snapshot.sh/restore.sh— explicit Hermes state preservation across tear-down/bring-up cycles.download-traces.sh— pull ATIF trace records from/tmp/atif/inside the sandbox into a host-side tarball. See Capturing ATIF traces for the env knobs.host-tls-proxy.py— optional plain-HTTP forwarder for hosts where the sandbox can't validate the inference endpoint's TLS chain (corporate VPN, split-horizon DNS, mkcert). See docs/host-tls-proxy.md.autoheal/— optional user-systemd installer, watchdog, response monitor, sanity checker, and unit templates. See docs/auto-heal.md.
- Generates and discards: sed-patched
.Dockerfile.stagedand.policy.staged.yamlfiles at the example dir root. OpenShell does the actual build; we patch ARG defaults beforehand becauseopenshell sandbox createdoesn't expose--build-arg, and we patch the GitHub read-only repo scope from.envbefore applying the policy.
The example's Dockerfile drops the upstream COPY nemoclaw-blueprint/ step —
nothing in the Hermes runtime reads /sandbox/.nemoclaw/blueprints/, so this
example is fully self-contained and never needs a NemoClaw checkout.
The Dockerfile inherits the pinned NemoClaw v0.0.105 Hermes sandbox base, then
installs a pinned Hermes source revision
(03fa32c92dd445eb64c7f67434dd91b32c40701d, package 0.20.0) with NeMo
Relay 0.7.2. Hermes's native observability/nemo_relay plugin handles scopes
and export in process, so the image needs no separate Relay installation or
process.
Setting PHOENIX_COLLECTOR_ENDPOINT is a separate opt-in for live
OpenInference egress: when present, 03-sandbox.sh bakes the URL into the
image so the agent streams traces to a Phoenix collector. The collector is
included in extras/docker-compose.yml and runs
on host port 6006.
In local mode, NeMo Relay writes an ATIF (Agent Trajectory Format) record to
/tmp/atif/ when Hermes finalizes a session and closes its top-level Agent
scope. Finalization happens on an explicit /new or /reset, CLI/TUI exit, or
configured gateway expiry—not after every conversational turn. That directory
is ephemeral—it lives on the sandbox's writable layer and is destroyed by
tear-down.sh—so capture before destroying the sandbox if you want to keep
the traces.
For production / always-on capture, set ATIF_EXPORT_MODE=relay (with
ATIF_RELAY_BACKEND=minio or s3) to have NeMo Relay upload completed
trajectories to S3-compatible storage via a host-side relay. The sandbox never holds real
AWS credentials; OpenShell's provider store manages a per-sandbox bearer
token instead. A successful remote POST does not also create a local file. If
all configured remote targets fail, NeMo Relay 0.7.2 writes a recovery copy
to /tmp/atif/. See docs/atif-export.md for setup, IAM
template, and the auth model.
$ bash scripts/download-traces.shWrites $EXAMPLE_DIR/.traces/atif-{ISO-timestamp}.tar.gz plus a JSON
manifest sidecar. The tarball path is printed on stdout (progress goes to
stderr), so callers can capture it:
$ TRACE=$(bash scripts/download-traces.sh)Two env vars answer the "from where / to where" questions and can be overridden at the call site:
| Env var | Default | What it controls |
|---|---|---|
SANDBOX_NAME |
hermes-direct |
Which OpenShell sandbox to pull /tmp/atif/ from. Shared with the rest of the example's scripts (defined in _lib.sh). |
TRACES_DIR |
$EXAMPLE_DIR/.traces |
Host-side directory the tarball is written to. |
If /tmp/atif/ is empty when the script runs (for example, no session has
finalized, or remote export succeeded), the script still emits a
valid empty tarball whose manifest carries an explanatory note — downstream
tooling never has to special-case "no file."
Lifecycle note: no automatic rotation — files accumulate until
sandbox teardown. For long-lived sessions, prune manually inside the
sandbox, e.g. find /tmp/atif -type f -mtime +7 -delete.
- Docker daemon running.
openshellCLI on PATH (installed transitively by the NemoClaw installer).providers_v2_enabledset globally on the OpenShell gateway (openshell settings set --global --key providers_v2_enabled --value true --yes)..envpopulated with the credentials below.- (Optional) If your network performs TLS interception (e.g. an
SSL-inspecting proxy), place the inspection CA certificate(s) as
.crtfiles in the example-rootcerts/directory before runningbring-up.sh. Additional roots installed in the host's standard local CA directory are staged automatically. Otherwise leave the directory empty. Seecerts/README.mdfor details.
Four of the five providers use custom v2 profiles in providers/
and are attached to the sandbox directly. Inference goes through OpenShell's
built-in nvidia v2 profile + openshell inference set + inference.local
routing for gateway-side hardening (streaming timeout, header sanitization,
model-ID enforcement, credential never enters the sandbox env). The custom
providers/compatible-endpoint.yaml is
checked in as a forward-looking placeholder for when OpenShell adds inference-
route auto-creation from inference_capable v2 profiles; until then it's not
imported. The agent calls each non-inference upstream directly with a placeholder
bearer header; the OpenShell L7 proxy substitutes a live token on egress.
| Provider name | --type |
Credential env var | Required? |
|---|---|---|---|
compatible-endpoint |
nvidia (built-in v2; consumed via openshell inference set, not attached to the sandbox directly) |
NVIDIA_API_KEY (populated from OPENAI_API_KEY / COMPATIBLE_API_KEY at provider-create time). URL: NEMOCLAW_ENDPOINT_URL → NVIDIA_BASE_URL provider config. Routing via inference.local. |
Required for inference. Missing credentials stop setup before the sandbox build unless the explicit offline preflight bypass is selected. |
<sandbox>-outlook |
nemoclaw-outlook-email |
MS_GRAPH_ACCESS_TOKEN (auto-rotated by the gateway from the registered refresh token). Refresh material: OUTLOOK_TENANT_ID, OUTLOOK_CLIENT_ID, refresh_token (cached from device-code login). |
Optional. Created only when the Outlook block is fully populated; partial config is rejected. At least one of Outlook or Slack must be configured. |
<sandbox>-slack |
nemoclaw-slack |
SLACK_BOT_TOKEN (Web API) + SLACK_APP_TOKEN (Socket Mode) |
Optional. Before provider creation, setup verifies that the app token can call apps.connections.open with connections:write. At least one of Outlook or Slack must be configured. |
<sandbox>-github |
nemoclaw-github |
GITHUB_TOKEN |
Optional but recommended. Enables authenticated live GitHub REST reads. The sandbox receives only the OpenShell placeholder; policy.yaml further limits use to repo-scoped GET routes from approved binaries. |
<sandbox>-atif-export-relay |
nemoclaw-atif-export-relay |
ATIF_RELAY_AUTH_TOKEN |
Created and attached only when ATIF_EXPORT_MODE=relay. Allows the Python ATIF bridge to send POST /atif to the configured host relay; the provider owns the endpoint, path, binary, private-IP, and credential restrictions. |
The compatible-endpoint provider is not prefixed with the sandbox name — it's a
shared inference provider attached via --provider compatible-endpoint on sandbox
create, the same way every other v2 provider is attached. The agent dials the upstream
host directly; the L7 proxy substitutes the OPENAI_API_KEY placeholder on egress.
Sandbox network policy (policy.yaml) layers on top of the per-provider
endpoint scopes above. For most providers the profile is the sole source of policy;
policy.yaml only carries restrictions that the v2 ProviderProfile schema can't
express today — specifically per-path allow rules (used to scope the NVIDIA inference
API to specific /v1/* paths and GitHub reads to one repo via GITHUB_READONLY_REPO)
and credential-less host-routed services (Phoenix collector, Source-ETL API). Surviving
network_policies blocks in policy.yaml carry load-bearing comments explaining
why they can't be folded into provider profiles. Fully-redundant blocks (e.g. the
former slack block) have been removed.
GITHUB_TOKEN is attached as <sandbox>-github for live sandbox GitHub reads.
The sandbox sees only an OpenShell placeholder;
the raw token is resolved by the proxy on egress. GitHub write attempts are
still blocked by the applied policy, which allows only selected GET paths
under api.github.com/repos/$GITHUB_READONLY_REPO. If you keep the optional
host GitHub mirror enabled, it also reads GITHUB_TOKEN for API rate limits.
The live GitHub policy is repo-scoped. To change the repo the sandbox can read:
- Set
GITHUB_READONLY_REPO=owner/repoin.env. - For practical API rate limits, set
GITHUB_TOKEN. Private repos require a token with access to the target repo; public repos can be read without a token but use GitHub's lower unauthenticated API limits. - Recreate the sandbox so
scripts/03-sandbox.shcan stage the Dockerfile env and apply a policy for the new repo:
bash scripts/tear-down.sh
bash scripts/bring-up.shFor normal repo changes, do not edit policy.yaml by hand; 03-sandbox.sh
patches the __GITHUB_READONLY_REPO__ placeholder before applying the staged
policy. After bring-up, verify the live repo path from the host shell:
set -a; . ./.env; set +a
openshell sandbox exec --name "${SANDBOX_NAME:-hermes-direct}" -- sh -lc \
'/usr/bin/python3 /sandbox/.hermes-data/skills/github-readonly-live/scripts/github_readonly.py get . --fields full_name,default_branch,open_issues_count'GITHUB_READONLY_REPO controls only live REST reads through
github-readonly-live. The host-side ETL mirror is independent and disabled by
default. Set SOURCE_ETL_GITHUB_ENABLED=1 and optionally
SOURCE_ETL_GITHUB_REPO=owner/repo when you want mirrored GitHub
discussions/history, then rerun
bash scripts/00-host-services.sh. Existing mirror database/state is preserved
unless you remove the compose volumes.
| Var | Default | What it does |
|---|---|---|
SANDBOX_NAME |
hermes-direct |
OpenShell sandbox name. Default avoids clobbering nemoclaw-hermes. |
OPENSHELL_GATEWAY |
openshell |
Gateway name. The default matches the package-managed OpenShell installer. Use snap-docker when following the snap setup. |
OPENSHELL_GATEWAY_ENDPOINT |
auto (https://127.0.0.1:17670 for openshell, http://127.0.0.1:17670 for snap-docker) |
Override the local gateway endpoint if you registered it under a different URL. |
NEMOCLAW_MODEL |
nvidia/nemotron-3-super-120b-a12b |
Inference model passed to openshell inference set. |
NEMOCLAW_INFERENCE_PREFLIGHT |
1 |
Requires one bounded structured tool call before sandbox creation, then verifies that OpenShell activated the requested provider and model. Remote endpoints must use HTTPS. For http://host.openshell.internal:<port>, the host-side check safely uses the same listener through 127.0.0.1:<port>. Non-empty proxy and CA environment variables are preserved; unset CA overrides remain unset so the platform trust store still works. Set to 0 only for intentional offline setup or an endpoint that cannot support verification. |
NEMOCLAW_INFERENCE_PREFLIGHT_TIMEOUT_SECONDS |
10 |
Maximum time allowed for the preflight request. |
NEMOCLAW_SLACK_RICH_BLOCKS |
true |
Render supported semantic Markdown with Hermes's native Slack Block Kit renderer, including table blocks. Set to false for text-only output. Interactive clarification buttons remain available. Only true or false is accepted. Rebuild the sandbox after changing it. |
NEMOCLAW_ENDPOINT_URL |
https://integrate.api.nvidia.com/v1 |
Upstream base URL for the compatible-endpoint provider. (OPENAI_BASE_URL is also accepted as a fallback.) |
NEMOCLAW_HOST_TLS_PROXY_UPSTREAM |
(none) | Optional HTTPS origin for the host TLS proxy. Required when NEMOCLAW_ENDPOINT_URL uses host.openshell.internal:18080 and auto-heal should manage that proxy. |
NEMOCLAW_HOST_TLS_PROXY_PORT |
18080 |
Host listener port for the optional TLS proxy. |
NEMOCLAW_HOST_CA_BUNDLE |
/etc/ssl/certs/ca-certificates.crt |
Absolute path to a readable regular-file host CA bundle mounted read-only into the GitHub/forum ETLs and ATIF relay. Override when the supported Ubuntu host stores its trusted bundle elsewhere. |
COMPATIBLE_API_KEY |
(none) | Inference API key. Mirrors NemoClaw's REMOTE_PROVIDER_CONFIG.custom. (OPENAI_API_KEY is also accepted.) |
GITHUB_TOKEN |
(none) | Optional GitHub token for authenticated live REST reads. Also feeds the optional host GitHub mirror. |
GITHUB_READONLY_REPO |
NVIDIA/OpenShell |
The only repo allowed by the live GitHub REST policy, formatted as owner/repo. Recreate the sandbox after changing it. |
SOURCE_ETL_GITHUB_ENABLED |
0 |
Set to 1 to start the host-side GitHub mirror. A live-read GITHUB_TOKEN alone does not enable the ETL. |
SOURCE_ETL_GITHUB_REPO |
NVIDIA/NemoClaw |
Host-side GitHub mirror repo for source-etls. This is independent of GITHUB_READONLY_REPO. |
OUTLOOK_LOGIN_CACHE |
1 |
Controls the Microsoft refresh-token cache at .bootstrap/cache/ms-graph-token.json. 1 = use the cache (auto-refresh on staleness, ~90 days). 0 = skip the cache entirely (device-code every bring-up, nothing on disk; use on shared workstations or security-sensitive contexts). 2 = force device-code login and rewrite the cache. The gateway-side encrypted credential copy is unaffected by this knob. |
PHOENIX_COLLECTOR_ENDPOINT |
(none) | Set to e.g. http://host.openshell.internal:6006/v1/traces to stream OpenInference traces to a Phoenix collector. ATIF export is independent: local mode writes completed scopes to /tmp/atif/; relay mode sends them remotely and uses /tmp/atif/ only for recovery after all remote targets fail. |
PHOENIX_PROJECT_NAME |
default |
Sets openinference.project.name on every exported span so Phoenix routes traces to a named project. Override per-build to keep multiple deployments separate in the same Phoenix instance. |
The plumbing checks below confirm the bridge and skill scripts are wired correctly. For an end-to-end walkthrough that exercises each skill via Slack DM and Outlook email, see docs/verify-functionality.md. For a cross-channel, multi-user demo where one user teaches the agent a new skill and a different user invokes it from a different channel after a full sandbox rebuild, see docs/collective-wisdom.md.
$ set -a; . ./.env; set +a
$ openshell sandbox list # hermes-direct should be ready
$ openshell sandbox exec --name hermes-direct -- \
curl -sf http://localhost:8642/health # {"status":"ok",...}
$ openshell sandbox exec --name hermes-direct -- \
ls /usr/local/lib/nemoclaw-bridges/outlook/ # bridge present
# Verify the v2 outlook provider's gateway-managed refresh works end-to-end.
# The L7 proxy substitutes the placeholder with a live access token on egress.
$ openshell sandbox exec --name hermes-direct -- \
curl -sS -H "Authorization: Bearer openshell:resolve:env:MS_GRAPH_ACCESS_TOKEN" \
'https://graph.microsoft.com/v1.0/me' | head -c 300
$ openshell sandbox exec --name hermes-direct -- env \
OUTLOOK_TARGET_MAILBOX="$OUTLOOK_TARGET_MAILBOX" \
/usr/bin/python3 /sandbox/.hermes-data/skills/outlook-email-search/scripts/search_emails.py \
--query "nemoclaw" --since 7d # {"ok": true, "count": N, ...}Note 1:
openshell sandbox execrequires--name <SANDBOX>and a--separator before the command — without them, the sandbox name is parsed as the command itself (hermes-direct: command not found).Note 2: The verification points the search at
OUTLOOK_TARGET_MAILBOX(the bot's own mailbox) because the delegated token always has access to it. SubstitutingOUTLOOK_REPLY_TOto query your personal mailbox via shared-folder access requires you to have granted the bot account delegate access in Outlook (Outlook → File → Account Settings → Delegate Access, or send a folder-share invitation); without that, Graph returnsHTTP 403: Cannot find row based on condition.
$ bash scripts/tear-down.shRemoves the sandbox, the Outlook/GitHub/Slack providers, and any leftover staged Dockerfile/policy files. Does not destroy the gateway or stop host services (phoenix, postgres, ETLs, postgrest) by default — those are typically long-lived. Opt-in flags (mutually exclusive):
--stop-host-services— also stop the extras/ stack (volumes preserved; delegates to00-host-services.sh down).--purge-host-services— also stop the stack AND wipe its volumes. Forces ETL re-scrape on the nextup. Delegates to00-host-services.sh down --volumes.
Manual cleanup for less-common operations:
openshell gateway destroy --name "${OPENSHELL_GATEWAY:-openshell}"— destroy the gateway (substitutesnap-dockerif you registered it under that name).openshell provider delete compatible-endpoint— remove the shared inference provider.
To stop just the host services without removing the sandbox:
$ bash scripts/00-host-services.sh downAdd --volumes (or -v) to also wipe the source-etls Postgres data (forces ETL re-scrape on the next up). Outlook re-auth is independent — re-run bash scripts/bring-up.sh with OUTLOOK_LOGIN_CACHE=2.
What survives a tear-down.sh && bring-up.sh cycle by default:
- Postgres ETL data — backed by the named Docker volume
source-etls-postgres-datain extras/docker-compose.yml. Survives unless you opt in to--purge-host-services(or runbash scripts/00-host-services.sh down --volumesdirectly). - Host services state (phoenix's traces) — also volume-backed.
- Microsoft refresh token (in
.bootstrap/cache/ms-graph-token.json; ignored by.gitignore) — survives tear-down/bring-up cycles, auto-refreshed on staleness. SetOUTLOOK_LOGIN_CACHE=2to force a fresh device-code login + cache rewrite, orOUTLOOK_LOGIN_CACHE=0to skip the cache altogether.
What does not survive by default:
- Hermes's accumulated state under
/sandbox/.hermes-data/(memories, sessions, learned skills, scheduled cron, conversation history). The sandbox container is destroyed on tear-down; the writable layer goes with it.
This example ships scripts/snapshot.sh and scripts/restore.sh for explicit state preservation. OpenShell's CLI does not expose a sandbox stop/start pair (the lifecycle is create / delete), so snapshot-as-tarball is the durable path.
# 1. Capture state from a running sandbox.
$ bash scripts/snapshot.sh
$ ls .snapshots/ # tarball + manifest.json
# 2. Tear down completely.
$ bash scripts/tear-down.sh
# 3. Bring up a fresh sandbox.
$ bash scripts/bring-up.sh
# 4. Re-hydrate. Defaults to the most recent snapshot in .snapshots/.
$ bash scripts/restore.sh
# 5. Reconnect — Hermes recalls what it learned in step 1.
$ openshell sandbox connect hermes-directTo pin a specific snapshot instead of the latest, pass the path:
bash scripts/restore.sh .snapshots/2026-05-07T19-03-22Z.tar.gz.
scripts/snapshot.sh excludes obvious
credential-bearing filenames (.env, *secret*, *token*,
auth-profiles*, etc.), the legacy writable nvteam/ registry location, and
recognizable copies of persona-authorities files. The filter is name-based,
not content-aware. A snapshot can still contain sensitive conversation,
session, memory, or learned-skill content. Treat every snapshot as sensitive;
inspect and independently sanitize it before sharing.
scripts/restore.sh also rejects an archive that contains
a recognizable private authority registry path.
The .snapshots/ directory is .gitignore'd.
For a hands-on demo of this in action — including a learned skill surviving a full rebuild and being invoked by a different user from a different channel — see docs/collective-wisdom.md.
