Skip to content

feat: add remote-only and stateless serverless controller modes - #163

Merged
fwerkor merged 4 commits into
mainfrom
feat/disable-local
Aug 11, 2026
Merged

feat: add remote-only and stateless serverless controller modes#163
fwerkor merged 4 commits into
mainfrom
feat/disable-local

Conversation

@fwerkor

@fwerkor fwerkor commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • add disable_local / LOCAL_SHELL_MCP_DISABLE_LOCAL (default: false) and tighten remote-only behavior across MCP tools and WebUI
  • add stateless_controller plus pluggable file, memory, and optional Redis control-state backends so the controller does not require a persistent volume
  • migrate remote registry, OAuth clients/codes, jobs/logs, audit payloads, dynamic MCP registry, todo state, and UI local credentials onto the state backend
  • make remote→remote file/directory transfer never stage payloads on controller disk
  • add transfer strategies: opt-in worker-to-worker direct HTTP, optional S3-compatible presigned transfer, and bounded-memory controller relay fallback; auto tries direct → object store → relay
  • keep controller-managed remote transfer jobs listable/tailable/stoppable/retryable in remote-only mode while local shell jobs remain unavailable
  • block controller-local stdio dynamic MCP and local WebUI TUI/PTTY paths when local access is disabled
  • document serverless/Redis and transfer configuration in English, Simplified Chinese, and Traditional Chinese

Serverless scope

stateless_controller=true removes the persistent-volume requirement. The default memory backend is intentionally ephemeral: cold starts invalidate pending remote invites and worker identities as well as other control state. Use state_backend=redis when that state must survive cold starts. Active remote RPC queues/futures still live in one controller process, so remote-worker deployments should currently run a single active controller replica rather than horizontally load-balanced replicas.

Validation

  • PYTHONPATH=src pytest -q — 697 passed, 1 skipped
  • conservative combined coverage (unit + REST smoke + MCP OAuth smoke, excluding CI-only UI smoke) — 93.39% total, every module >= 90%
  • PYTHONPATH=src ruff check .
  • PYTHONPATH=src python scripts/export-tools-json.py
  • python scripts/check-doc-i18n.py
  • mkdocs build --strict
  • git diff --check
  • secret_scan — only pre-existing/test fixture generic-assignment matches

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b8dd12fd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/local_shell_mcp/settings.py
Comment thread src/local_shell_mcp/human_ui.py
Comment thread src/local_shell_mcp/tools.py
Comment thread docs/reference/configuration.md
Comment thread src/local_shell_mcp/tools.py
Comment thread src/local_shell_mcp/tools.py Outdated
@fwerkor fwerkor changed the title feat: add remote-only mode feat: add remote-only and stateless serverless controller modes Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5767c65eae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/local_shell_mcp/remote.py Outdated
Comment thread src/local_shell_mcp/remote.py Outdated
Comment thread src/local_shell_mcp/settings.py Outdated
Comment thread src/local_shell_mcp/tools.py
Comment thread src/local_shell_mcp/peer_transfer.py Outdated
Comment thread src/local_shell_mcp/tools.py Outdated
Comment thread src/local_shell_mcp/oauth.py Outdated
Comment thread src/local_shell_mcp/audit.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28e4a00aef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/local_shell_mcp/oauth.py
Comment thread src/local_shell_mcp/todo_ops.py
Comment thread src/local_shell_mcp/remote.py Outdated
Comment thread src/local_shell_mcp/audit.py
@fwerkor
fwerkor merged commit cab0554 into main Aug 11, 2026
41 checks passed
@fwerkor
fwerkor deleted the feat/disable-local branch August 11, 2026 07:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90aebaa117

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

path.parent.mkdir(parents=True, exist_ok=True)
data = {
"version": 1,
"workers": [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist pending invites in the shared registry

When state_backend=redis and the controller cold-starts after invite generation but before enrollment, the invite is rejected because create_invite() calls _save_registry_unlocked(), but this payload contains only workers and the loader likewise reconstructs only workers, leaving self.invites empty. This breaks remote enrollment across exactly the serverless cold starts Redis is intended to survive; serialize and restore unexpired pending invites as well.

Useful? React with 👍 / 👎.

Comment on lines +279 to +280
if get_settings().state_backend != "file":
get_state_store().write_bytes("dynamic-mcp.json", (payload + "\n").encode("utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Lock dynamic registry read-modify-write transactions

When two Redis-backed controller instances concurrently mutate different dynamic MCP servers, each process's asyncio.Lock protects only that instance: both can load the same snapshot and this unconditional full-registry write then causes the later writer to discard the other registration or configuration update. Hold the state store's distributed lock across each complete load/modify/save transaction, including refresh updates.

Useful? React with 👍 / 👎.

Comment on lines +508 to +511
with state_lock("audit.jsonl"):
existing = store.read_bytes("audit.jsonl") or b""
store.write_bytes("audit.jsonl", existing + encoded.encode("utf-8"))
_enforce_state_audit_storage_limit(settings.max_audit_log_bytes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize audit payload creation under the shared lock

When two Redis-backed replicas emit audit values above the inline threshold concurrently, _serialize_audit_value() writes each external payload before this distributed lock is acquired. The replica entering the lock first calls _enforce_state_audit_storage_limit(), which immediately deletes every currently unreferenced audit-payloads/* key, so it can delete the other replica's payload before that replica appends its reference; full audit queries then report the payload as unavailable. Acquire the shared lock before externalizing the record or retain fresh unreferenced payloads.

Useful? React with 👍 / 👎.

Comment thread pyproject.toml
Comment on lines +47 to +49
redis = [
"redis>=5.0.0,<7",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install Redis support in the published container

In the documented Docker Compose serverless setup, setting LOCAL_SHELL_MCP_STATE_BACKEND=redis cannot work with the published image: Redis is defined only in this optional extra, while the Dockerfile installs .[dev] and requirements-agent.txt contains no Redis client. The first state-store access therefore raises the missing-redis runtime error and breaks OAuth and other control-plane operations; install the redis extra in the official runtime image that now exposes this configuration.

Useful? React with 👍 / 👎.

Comment on lines +1316 to +1317
"--max-time",
str(_worker_curl_timeout(timeout_s)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the configured peer-transfer timeout

When remote_peer_transfer_timeout_s is configured above remote_job_timeout_s, direct transfers still terminate at the shorter job timeout because this new upload path passes the peer timeout through _worker_curl_timeout(), which clamps it to remote_job_timeout_s. The controller and destination receiver can remain valid for the longer configured interval while the source curl exits early, so large transfers fail or unnecessarily fall back; either avoid this clamp for peer transfers or validate the two settings' relationship.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant