Skip to content

✨ 新增 sctl 本地守护进程与 MCP 桥接 - #3

Open
CodFrm wants to merge 14 commits into
mainfrom
feat/sctl-daemon
Open

✨ 新增 sctl 本地守护进程与 MCP 桥接#3
CodFrm wants to merge 14 commits into
mainfrom
feat/sctl-daemon

Conversation

@CodFrm

@CodFrm CodFrm commented Jul 21, 2026

Copy link
Copy Markdown
Member

说明

将原 main 分支中的 sctl 守护进程实现通过 PR 重新引入;仓库 main 已重置为仅包含简单 README 的干净基线。

此 PR 替代因分支历史被重建而无法重新打开的 #2

关联开发与讨论:

主要内容

  • sctl CLI 与本机守护进程
  • WebSocket 桥接、认证、配对、授权与限流
  • MCP server 与本机控制 API
  • 协议、威胁模型、测试及 CI/发布配置

CodFrm added 14 commits July 21, 2026 10:26
Replace per-client pairing/scope/revocation with a flat model: enrollment establishes the long-term key K once, and the CLI and every MCP agent inherit it over the enrolled channel. clientId becomes an audit-only label.

- remove client store, cached MCP identity, per-client pairing/sync/revoke
- rename `sctl pair` -> `sctl connect`; control /pair-ext -> /enroll
- `sctl mcp` drops the pair subcommand; --name is now an audit label
- mcpserver exposes all tools (no scope filter)
- add an Origin whitelist (chrome-extension:// etc.) as a cheap web pre-filter; the handshake stays the real gate
- source reads are no longer CLI-exempt (source-read policy gates both)
- rewrite protocol.md / threat-model.md / architecture.md / README / AGENTS
…pair/client

Mirror the extension-side protocol change byte-for-byte: envelopeTypes becomes {session, bridge} and the flat-trust-dead pair.*/client.* types are removed. protocol.go's EnvelopeTypes field becomes a nested struct so Load() parses the new shape. protocolVersion unchanged (no wire-format change).
The SDK gains full support for protocol revision 2026-07-28: a stateless
model with per-request _meta, server/discover replacing the initialize
handshake, MRTR, subscriptions/listen, and cacheable list results.

No source change is required. sctl serves MCP over stdio, so the v1.7.0
constraint that streamable HTTP only accepts 2026-07-28 when
StreamableHTTPOptions.Stateless is set does not apply, and no API used by
internal/client/mcpserver broke.

Verified by driving the real binary over stdio with raw JSON-RPC, since
the package tests put go-sdk on both ends and therefore cannot observe
which revision is actually served: server/discover advertises
2026-07-28 and tools/list succeeds with no handshake, while a legacy
initialize client still negotiates 2025-06-18 and gets the same tools.
…l.json

Add scripts:edit:request scope and the scripts.edit.request (write,
approval) / scripts.source.grep (reuses scripts:source:read, disclosure)
actions to the bridge protocol mirror. Declarations only; no handling
wired up yet.
…output)

Fold `scripts list/info/source` into a single kubectl-style `get`
command and make the leading `scripts|script|sc` resource word
optional on get/delete/enable/disable. Replace the global --json
bool with -o/--output (table default / json / source), remove `rm`
(del is now delete's only alias), and wire --lines through to
scripts.source.get for get <uuid> -o source.
… in RunE

The verb commands validated arg count through stripResourceWord but read the
raw args[0], so `sctl delete sc u1` dispatched uuid="sc" — deleting the wrong
script (or NOT_FOUND) instead of u1.

Arg-count errors now return ExitError{exitError}: a bare error is folded into
exit 1 by main, and 1 is reserved for "user rejected in the browser", so a
caller could not tell a typo from a rejection.

printScriptSource decodes code into a pointer so an empty window (--lines over
blank lines) prints nothing rather than falling back to dumping the result
envelope into a redirected .user.js.

The CLI tests grew real guards: the leaves that assert a rejection now stub the
daemon and pin the dispatched action and uuid, so they no longer pass merely
because an unreachable daemon also exits 3, and parseLinesFlag is covered
directly.
spawnServeProcess exec'd os.Executable() with "serve", assuming the running
program is sctl. It is not whenever the client packages are linked into another
binary. Under `go test` os.Executable() is the test binary, so every leaf that
reached ensureDaemon without a live daemon re-ran the whole test package
detached — and each of those re-runs spawned again. Running `go test ./...`
grew to thousands of processes and exhausted the system's fork limit.

The children also inherited SCTL_BRIDGE_ADDR, so they issued /control/call
requests against the parent's stub server and overwrote the captured request
other tests assert on, which is why TestGrepInputMapping and TestGrepOutput
failed intermittently. With the guard in place internal/cli drops from 81s
(failing) to 0.4s.

`go run ./cmd/sctl` builds a temporary binary that is still named sctl, so
auto-launch during development is unaffected.
`sctl edit <uuid>` requests a content-anchored edit: the edits travel alone, so
the source is never read first and never uploaded. Edits come either as a JSON
array via -f (- reads stdin) or as repeated --replace/--with pairs, whose values
accept @path to read from a file and @@ to escape a literal leading @ — user
script metadata lines start with @, so the escape is not hypothetical.
--replace uses StringArray rather than StringSlice: the latter splits on commas
and would tear an anchor like "f(a, b)" into two edits.

`sctl grep <uuid> <query>` searches one script's source. The query is a literal
substring unless -E is given; -i, -C and -m map to ignoreCase, contextLines and
maxMatches, and are only sent when actually given so the defaults live on the
extension side alone. Output follows grep -n, with truncation and skipped-line
notices on stderr so stdout stays pipeable. No match is not an error: exit stays
0, because exit 1 already means the user rejected the request.

The MCP side registers scripts_edit_request and scripts_source_grep and gives
scripts_source_get its line-window fields, closing the tool-count assertion that
went red when protocol.json declared the two actions. The grep tool description
spells out that the default mode matches literally, since a model will otherwise
assume the query is a pattern and be puzzled by `.*` finding nothing.
Command help and flag descriptions, stdout/stderr output, error and log
messages, and the MCP tool descriptions and input schemas are now English.
Code comments and Convey test titles stay Chinese; the test assertions on
the affected CLI output are updated to match.
The command table still advertised `sctl scripts list / info / source` and a
top-level `rm`, and the global-flag line still documented `--json` — all of
them removed when the command surface went resource-oriented. docs/architecture.md
carried the same stale diagram plus an internal/cli listing naming a scripts.go
that no longer exists, and docs/verification.md's `./sctl scripts list` was an
executable verification step that now hits "unknown command" instead of the
exit 3 it claims to demonstrate.

protocol.md gains the two new actions (scripts.source.grep, scripts.edit.request),
the line window on scripts.source.get, and the constraints a client actually
hits: literal-by-default grep, overlap-counted anchor uniqueness, the 100-edit
batch cap. Also mirrors that cap into the MCP edit schema as maxItems so an
agent learns the bound locally instead of from a rejected request.

README is translated wholesale rather than left half-Chinese: its command table
and flag section had to be rewritten anyway, and docs/ plus AGENTS.md are
already English.

Two stale facts found while checking the rest: `sctl status` reports no
per-client pairing under flat trust, and the extension's audit component is
external-access, not local-access.
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