English | 繁體中文
Require human review before an agent expands an OpenShell sandbox's network permissions. Each approval binds one exact operation to its gateway, requester, workspace, sandbox, resource version, destination and executable, and can be consumed only once.
Status: real OpenShell v0.1.6 integration verified; real Keyper authentication pending. A separate Ubuntu VM verified the complete flow: reject an unapproved change, approve through the private operator MCP, submit through the agent MCP, activate policy version 2, and establish HTTPS connectivity. Other destinations and replayed submissions remained blocked. Windows and Linux each passed 73 tests. See the recorded evidence.
This independent experiment leaves OpenShell source unchanged and does not imply NVIDIA endorsement or an official partnership.
| Your goal | Requirements | Real policy changes | Start here |
|---|---|---|---|
| Learn requests, human decisions and single use | Git, Python 3.11+, uv | None; simulated | Quickstart |
| Develop MCP/Keyper interfaces and test gRPC | The above, plus the openshell extra | None; a gateway test peer | Protocol tests |
| Verify a real OpenShell sandbox | Separate Linux host, Docker, official v0.1.6 binaries | Creates a test sandbox and updates its policy | Live validation |
Start with the mock demo. It requires no Keyper account, API key, Docker or OpenShell.
The first dependency installation needs network access. uv.lock pins dependencies.
Run the commands below from the repository root unless instructed otherwise.
flowchart TB
subgraph AgentHost["Agent host"]
A["Agent / MCP client"]
AM["Agent MCP (stdio)<br/>Request, query, submit"]
A --> AM
end
subgraph OperatorHost["Private human operator host"]
H["Review UI / Keyper host"]
OM["Operator MCP (separate stdio)<br/>Review, challenge, decision"]
V["KeyperVerifier<br/>Verify assertion and decision binding"]
K["Keyper SDK / identity evidence<br/>Adapter pending"]
H --> OM --> V
V -. "Future integration" .-> K
end
subgraph TrustedLab["Trusted lab services"]
S["Approval service<br/>Operation fingerprint, expiry, roles"]
DB[("SQLite<br/>Approvals and audit records")]
I["OpenShell gRPC interceptor<br/>validate: compare and consume"]
S --> DB
I --> S
end
subgraph Runtime["Real OpenShell"]
G["Gateway: UpdateConfig RPC"]
B["Supervisor / sandbox<br/>Load policy, enforce network access"]
G -->|"Commit and deliver policy"| B
end
AM -->|"Request / query"| S
V -->|"Verified identity and decision"| S
AM -->|"Submit the stored approved operation"| G
G -->|"Final operation and authenticated principal"| I
I -->|"Allow / deny"| G
Connect the operator MCP to a trusted human-facing host, separately from the requesting agent. Lab services can share a local database; OS users who can directly access it are trusted operators. Production deployment requires permission separation between the agent, database and decision service.
sequenceDiagram
participant A as Agent MCP
participant S as Approval service / DB
participant H as Private operator MCP / human
participant K as KeyperVerifier
participant G as OpenShell gateway
participant I as gRPC interceptor
participant B as Sandbox
A->>S: request_policy_update(operation, reason)
S-->>A: request_id, pending, fingerprint, expiry
H->>S: get_policy_review(request_id, approve)
S-->>H: Complete operation and decision challenge
H->>K: decide_policy_update(assertion, context)
K-->>S: Verified subject and approval roles
S-->>H: approved or denied
A->>G: submit_approved_policy(stored operation)
G->>I: validate(final operation, principal)
I->>S: Exact match, expiry check, atomic consumption and audit
S-->>I: Consume once or reject
I-->>G: Allow or deny
G->>B: Commit and deliver the new policy
B-->>G: Report policy activation
approved describes the review outcome. The interceptor checks authorization again on the gateway path.
After gateway acknowledgement, verify runtime policy activation and connectivity separately.
Install Git, Python 3.11+
and uv.
On GitHub, select Fork → Create fork. Replace YOUR_GITHUB_USERNAME with your account:
git clone https://github.com/YOUR_GITHUB_USERNAME/openshell-keyper-lab.git
cd openshell-keyper-lab
python --version
uv --versionOn Linux, use python3 --version if that is your Python command.
A fork is your GitHub copy; cloning downloads the code to your computer.
Start with these commands on Windows, Linux or macOS:
uv sync --locked
uv run --locked python -m approval_lab.demo
uv run --locked pytest -qThe demo should end with:
PASS: no real network rule or OpenShell policy was changed.
It uses temporary SQLite storage, a real MCP stdio subprocess and a separate operator CLI process. It checks unapproved access, unauthorized identities, changed ports, a successful single use and replay. The demo database is removed on normal completion.
Without the openshell extra, expect 50 passed, 1 skipped. Installing that extra enables all
73 tests. Recorded cross-platform verification covers Windows and Linux.
To preserve the existing project Python environment, use:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-local-lab.ps1The script reuses an installed Python and creates a source copy, dedicated venv, cache, temporary
files and reports under .approval-lab/isolated-run/<run>/. Its reports/summary.json lists the
Python and database paths. The execution-policy option applies only to this child process;
the script does not change permanent policy or system PATH.
Folder organization uses your current Windows permissions and provides no OS/network isolation.
Use a separate Linux environment for live OpenShell testing.
These commands simulate policy changes. In PowerShell:
$request = uv run --locked approval-lab request --sandbox sandbox-demo --domain api.example.com --port 443 --reason "Read the example API" | ConvertFrom-Json
$requestId = $request.request_id
uv run --locked approval-lab status $requestId
uv run --locked approval-lab approve $requestId --identity demo-approver
uv run --locked approval-lab apply $requestId --sandbox sandbox-demo --domain api.example.com --port 443
uv run --locked approval-lab apply $requestId --sandbox sandbox-demo --domain api.example.com --port 443
uv run --locked approval-lab audit $requestIdThe first apply returns applied=true, simulated=true. The second returns already_consumed
and exit code 1. To deny, run uv run --locked approval-lab deny <request_id> --identity demo-approver.
On Linux/macOS, run request first and paste the returned ID into subsequent commands instead
of using the PowerShell variables.
| Test identity | Behavior |
|---|---|
demo-approver |
Can approve or deny |
demo-viewer |
Known identity without the approval role |
| Any other name | Identity verification fails |
These names are mock test selectors. The default database is .approval-lab/approvals.sqlite3.
Requests expire five minutes after creation. Use the same absolute --db path across processes.
Windows dedicated folder:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-local-lab.ps1 -OpenShellStandard development environment:
uv sync --locked --extra openshell
uv run --locked --extra openshell pytest -qExpect 73 passed. These tests use two real MCP stdio connections and gRPC with a gateway protocol peer. They do not create a real OpenShell sandbox.
uv run --locked approval-lab-mcp --db /absolute/path/to/approvals.sqlite3A stdio server waits for MCP client requests. Waiting in the terminal is normal; there is no web UI.
Press Ctrl+C to stop it. Example client configuration; replace the absolute paths:
{
"mcpServers": {
"approval-lab-mock": {
"command": "uv",
"args": [
"run", "--locked", "--directory", "C:/path/to/openshell-keyper-lab",
"approval-lab-mcp", "--db", "C:/path/to/approvals.sqlite3"
]
}
}
}This server exposes request_network_access and get_approval_status. Human decisions use
the separate CLI. Adapt configuration to your MCP client; the project does not edit client settings.
| Host | Tool | Purpose |
|---|---|---|
| Agent | request_policy_update(operation, reason) |
Request review of a complete UpdateConfig operation |
| Agent | get_policy_approval(request_id) |
Read status and the original review content |
| Agent | submit_approved_policy(request_id) |
Submit the stored approved operation to the configured gateway |
| Private operator / Keyper host | get_policy_review(request_id, approve) |
Obtain review content and the decision challenge |
| Private operator / Keyper host | decide_policy_update(request_id, approve, assertion) |
Verify identity evidence and roles, then record the decision |
Use agent-mcp.example.json for the agent, and keyper-operator-mcp.example.json on a separate, trusted operator host. Replace the Python, config and database paths. Share the exported agent schema and operator schema with integrators; tests compare them to runtime.
Use a separate, disposable Ubuntu 24.04 x86_64 host with Python 3.11+, uv and Docker Engine. Confirm your user has Docker access:
docker version
docker run --rm hello-worldThe verified environment used Docker 29.1.3 and OpenShell v0.1.6. The following steps download images, create a test sandbox, change its policy and probe connectivity. Run them on your dedicated test host.
bash scripts/prepare-linux-lab.shThis creates .approval-lab/linux-run/<run>/venv, runs the demo and 73 tests, then prints its
Python path. It installs no Docker or OpenShell system packages.
mkdir -p .approval-lab/openshell-bin
cd .approval-lab/openshell-bin
release=https://github.com/NVIDIA/OpenShell/releases/download/v0.1.6
for component in gateway sandbox; do
case "$component" in
gateway) archive=openshell-gateway-x86_64-unknown-linux-gnu.tar.gz ;;
sandbox) archive=openshell-sandbox-x86_64-unknown-linux-musl.tar.gz ;;
esac
curl -fL "$release/$archive" -o "$archive" || exit 1
curl -fL "$release/openshell-$component-checksums-sha256.txt" -o "$component-sha256.txt" || exit 1
grep " $archive$" "$component-sha256.txt" | sha256sum -c - || exit 1
tar -xzf "$archive" || exit 1
done
curl -fL "$release/openshell-0.1.6-py3-none-any.whl" -o openshell-0.1.6-py3-none-any.whl || exit 1
echo 'a0c5b0e9954ffdbdb4d34110d4a9d0d2dc57579a53e4a67372e0f8dd3b75cd81 openshell-0.1.6-py3-none-any.whl' | sha256sum -c - || exit 1
cd ../..The SDK checksum comes from the verified official release asset. See the environment receipt for sources and digests. Stop on checksum failure and check the downloaded content and release version.
bash scripts/build-live-workload.sh
lab_python="$(find "$PWD/.approval-lab/linux-run" -path '*/venv/bin/python' -print | sort | tail -n 1)"
"$lab_python" scripts/validate-live-openshell.py \
--bin-dir "$PWD/.approval-lab/openshell-bin" \
--sdk-wheel "$PWD/.approval-lab/openshell-bin/openshell-0.1.6-py3-none-any.whl"The script starts its own gateway and interceptor, creates a uniquely named sandbox, and drives
approval and submission through both MCP connections. On exit it stops its processes and requests
deletion of the test sandbox. The gateway uses host loopback and local-development authentication.
It prints Evidence directory: .... The directory's report.json should contain:
{
"real_gateway": true,
"real_keyper": false,
"success": true
}The report also records runtime_policy_active_version=2, before/after curl results and replay rejection.
The acceptance test probes the public example.com site. Use a service you control for production tests.
The recorded live report is language-neutral;
the detailed VM, TLS and concurrency guide is available in Traditional Chinese.
- Copy the config template to
.approval-lab/private/config.json. Set the actual gateway target, TLS CA and complete expected authenticated principal map. - Start the interceptor and configure the gateway's UpdateConfig/validate binding as shown below.
- Configure agent and operator MCP connections separately, using the same absolute DB path.
- Copy the operation template. Set the real workspace, sandbox, current resource version, destination and executable.
- Use
approval_lab.openshell_smoketo request review;--submitperforms the actual mutation. Mock decisions additionally require explicit--local-mock-keyper.
On Linux, create a private directory, edit the copied config, then start the interceptor:
mkdir -m 700 -p .approval-lab/private
cp examples/openshell/config.example.json .approval-lab/private/config.json
# Edit config.json before starting services.
uv run --locked --extra openshell python -m approval_lab.openshell_mcp interceptor \
--config "$PWD/.approval-lab/private/config.json" \
--db "$PWD/.approval-lab/private/bridge.sqlite3" \
--listen "unix:$PWD/.approval-lab/private/interceptor.sock"Add this to the gateway TOML, replacing the socket path with the absolute path visible to the gateway, then restart the gateway after the interceptor is ready:
[[openshell.gateway.interceptors]]
name = "approval-lab"
grpc_endpoint = "unix:///absolute/private/interceptor.sock"
order = 100
failure_policy = "fail_closed"
binding_policy = "exact"
timeout = "2s"
[[openshell.gateway.interceptors.bindings]]
rpc = "openshell.v1.OpenShell/UpdateConfig"
phases = ["validate"]Start the agent MCP through your client using its example configuration. Start the private operator
with a real verifier as described in the next section. submitted=true means gateway acknowledgement;
verify runtime activation and connectivity afterward.
For additional configuration examples, see the bridge guide (Traditional Chinese).
Live OpenShell acceptance explicitly uses --local-mock-keyper. Without a configured verifier,
the operator returns keyper_not_configured. The project makes no assumptions about real Keyper API
endpoints or token formats. Start with the adapter template:
def verify_decision(self, assertion: str, context: DecisionContext) -> Identity:
# Verify assertion, expiry and context.challenge with the official Keyper API.
# Resolve subject and roles from trusted authorization data, then return Identity.
...The operator UI calls get_policy_review, displays the complete operation and obtains the challenge.
Bind Keyper identity evidence to that challenge before calling decide_policy_update.
The challenge covers request ID, operation fingerprint, approve/deny and expiry. Returning the
challenge alone is not identity evidence.
Configure a trusted verifier factory:
uv run --locked --extra openshell python -m approval_lab.openshell_mcp operator \
--config /absolute/private/config.json \
--db /absolute/private/bridge.sqlite3 \
--keyper-verifier your_keyper_package.adapter:create_verifierThe network_approver role must come from trusted verification/authorization results.
Unverified assertions, agent-supplied roles and approved=true are not authorization evidence.
Keep assertions out of logs and Git.
| Change | Main files | Verify afterward |
|---|---|---|
| Agent/operator MCP tools | src/approval_lab/openshell_mcp.py |
Update tool schemas; preserve role separation |
| Keyper verification | keyper_adapter.py, examples/keyper_adapter_template.py |
Challenge, expiry, roles and failure rejection |
| Supported policy changes | src/approval_lab/openshell_policy.py |
Fingerprint the complete operation; reject changed approved content |
| Gateway calls/interception | openshell_bridge.py, openshell_wire.py |
Pinned protocol compatibility and fail-closed behavior |
| Persistence/single use | src/approval_lab/approval_store.py |
Atomic consumption and audit; one winner under concurrency |
| Basic mock | demo.py, cli.py, mcp_server.py |
Demo still runs without OpenShell |
| Live acceptance | scripts/validate-live-openshell.py |
Real gateway, runtime activation and connectivity |
Abbreviated Python paths in the table are under src/approval_lab/. Suggested workflow:
git switch -c feature/my-change
uv sync --locked --extra openshell
# After editing, run relevant checks and the complete protocol suite.
uv run --locked --extra openshell pytest -q
git diff --check
git add README.md README.zh-TW.md src tests examples docs scripts pyproject.toml uv.lock
git commit -m "feat: describe your change"
git push -u origin feature/my-changeAdd dependencies with uv add <package> and commit both pyproject.toml and uv.lock.
Tool changes also require updates to examples/openshell/*-tools.schema.json;
test_published_mcp_schemas_match_runtime checks consistency.
After changing live integration logic, rerun acceptance on a test host and refresh evidence.
The packager compares validated source hashes and will not automatically reuse an old
live_openshell_verified=true after those sources change. Keep both README languages in sync.
- The live bridge supports one sandbox-scoped
addRule: explicit workspace, nonzero version, one DNS host/TCP port and one absolute Linux executable path. - Approvals expire five minutes after creation and can be consumed once. A mismatch preserves the valid approval.
- Denial analysis and other observations may update resource version. A conflict after review requires a new request and approval.
- After consumption, gateway failure does not restore approval or trigger automatic retry. Cross-system transactions and production retry semantics remain to be designed.
- Enforcement currently covers UpdateConfig. CreateSandbox and other management/draft paths need separate governance.
- SQLite and mock identities assume a trusted lab host. Real Keyper verification and production permission isolation remain pending.
| Symptom | Action |
|---|---|
Repeated pytest EEEE..., temporary-directory access denied |
On Windows, use run-local-lab.ps1 for a fresh run-specific temporary directory |
| Missing grpc/google.protobuf | Install uv sync --locked --extra openshell and include the extra when running |
| MCP waits in the terminal | Connect an MCP client, or start with the demo |
| keyper_not_configured | Configure your verifier; use local-mock-keyper only in an isolated lab |
| change_mismatch/resource-version conflict | Read the current operation/version and request a new review |
| gateway_rpc_* or inactive policy | Inspect private gateway/interceptor logs, TLS, principal, version and runtime status |
| Fork lacks the author's VM/venv | .approval-lab/ is excluded from Git; recreate the environment or use your own Linux host |
control-local-vm.py manages an already provisioned VM. The portable-vm extra installs management
tools; it does not create a VM, download a disk or enable Windows features.
Optional Traditional Chinese guides:
- Mock walkthrough · Dedicated folder and cleanup
- OpenShell/Keyper connection · Live validation and VM
Also see integration boundaries and official protocol provenance.
Create a source delivery bundle:
uv run --locked python scripts/package-deployable.pyZIP, manifest and SHA-256 files go under .approval-lab/releases/. They exclude VM disks, caches,
databases, private keys and .env. These local runtime files are also excluded from Git and forks.
Report problems in GitHub Issues. Include OS, Python/uv versions, execution mode, reproduction steps, expected/actual results and a redacted error excerpt. To contribute, push a branch to your fork and open a pull request.
References: MCP Python SDK 1.x, OpenShell, Gateway Interceptors.