Address second-round review on PR #555:
- Move CloseAuditSink into the unconditional Execute defer so async remote
forwards are drained on BOTH success and failure paths. Cobra skips
PersistentPostRunE when RunE returns an error, which previously dropped
in-flight forwards for failed commands. Make CloseAuditSink idempotent via
sync.Once so the success-path hook and the defer can both call it.
- CSV export now returns a "文件:行号" error on malformed JSONL instead of
silently skipping the line and exiting 0.
- Add regressions: TestCloseAuditSinkDrainsOnErrorPath (error-path drain),
TestExportCSVFailsOnMalformedJSON (corrupt JSONL visible), and
TestAuditIdentityReresolvesOnProfileSwitch (per-profile Actor via an
injectable token loader seam).
Address PR #555 review:
- chain: derive prev_hash from file tail under cross-process flock, drop the
global .chain sidecar so per-day files stay independently verifiable and
concurrent dws processes cannot fork the chain
- forward: track async forwards with WaitGroup and add bounded Close(ctx) so
in-flight deliveries are not dropped on process exit
- actor: resolve Actor from the active runtime profile (profile-keyed cache)
- observability: BuildSink returns init errors; write/forward failures reported
to file log and to stderr when DWS_AUDIT_DEBUG is set
- cli: reject `audit tail --lines` < 1; check CSV writer/flush errors
- wire CloseAuditSink into PersistentPostRunE
- add regression tests for cross-date/cross-process chain, forwarder
wait/timeout, init-failure, tail validation, CSV export
Per req 83667761 (奕皓): messages sent through dws should carry the
「通过AI发送」badge by default, transparently flagging AI/CLI-sent messages.
- `--ai-tag` default flipped false → true on `chat message send` / `reply`, so
no flag / `--ai-tag` / `--ai-tag=true` all attach clawType (open edition
`openClaw`); only `--ai-tag=false` omits it (send as the user). The switch name
is unchanged. reply honors the same default (no longer leaks the wukong
clawType).
- skill chat.md: concise rule — default-on, pass `--ai-tag=false` to disable.
- tests: default now asserts clawType present; added an `--ai-tag=false` opt-out
case.
Every documented command / flag / example now matches `dws <svc> --help`:
- drop phantom commands (attendance class/group/vacation/..., contact label,
ding message list/receiver-status) that map to undeployed tools.
- fix runtime-failure flags (mail --body→--content, doc/calendar/minutes
pagination, chat send-by-bot @-flags, wiki member --users).
- route role/duty "who is responsible" queries to `aisearch person --dimension
duty` instead of the removed `contact label`.
- realign the multi/dingtalk-report skill to entry submit / inbox list / outbox
list.
`dws report entry submit --contents-file <f>` (and `--contents -` stdin) silently
submitted `contents:[null]`. Root cause: the `--contents-file` flag had no
transform, so its value mapped to an unused param while `--contents` stayed empty.
- file_read_json transform + a build-time report hook resolve --contents-file /
--contents - / @file natively in Go (priority: file > stdin > inline) and
declare a contents / contents-file one-of group so a file-only invocation is
no longer rejected at parse time.
- generalizes to @file / @- input for any structured JSON-array flag.
Override leaves whose backing MCP tool isn't actually deployed rendered in
`dws <svc> --help` but failed at invocation with "tool not found" (43 phantom
commands across 391 overrides; attendance declared 38, only 4 deployed).
BuildDynamicCommands now takes an existingTools oracle (CLI slug -> live tool
set from the tools/ cache). A leaf whose tool is missing from its resolved
server's set is marked Hidden; groups left childless collapse. Safety rails:
acts only when a server's tool set is KNOWN and non-empty (cold cache / overlay
/ plugin paths pass nil and no-op, never blanking the tree); serverOverride
leaves resolve against the target server; pipeline leaves are never hidden.
Adds scripts/dev/check-phantom-overrides.py as a publish-time gate, and
phantom_guard_test.go covering hide / cold-cache-keep / serverOverride / pipeline
/ empty-group-collapse.
Address reviewer feedback on #125:
- Hoist a single *cache.Store above the discovery fan-out so all HTTP and
stdio goroutines share one instance rather than each spawning its own in
registerHTTPServer/registerStdioServer. Atomic tmp+rename per cache key
keeps concurrent writers on distinct keys collision-free; global runtime
registries (AppendDynamicServer, RegisterStdioClient) already guard
themselves with a mutex. Added a comment pointing at those invariants.
- Relax cold-path timeouts to survive healthy cross-region endpoints and
Python/Node interpreter warm-up: HTTP 500ms → 1s (plain), 700ms → 1.5s
(auth), stdio 1s → 2s. A new DWS_PLUGIN_COLD_TIMEOUT env var overrides
all three with a single duration, registered in configmeta so it shows
up in `dws config --help`.
- Add internal/app/plugin_discovery_concurrency_test.go covering the
thread-safety claims end-to-end: 16 parallel SaveTools with distinct
keys, 32 parallel AppendDynamicServer, 32 parallel RegisterStdioClient,
plus TestResolvePluginColdTimeouts exercising defaults, a valid
override, an unparseable value, and a non-positive value. All pass with
go test -race.
Verified: go build ./... clean; targeted -race tests PASS in 1.5s;
./internal/{app,cli,cache,plugin}/... all green.
Review follow-up on #126: RemovePlugin used to call
setPluginEnabled(name, false), which left the plugin's key in
EnabledPlugins and never touched PluginConfigs. settings.json retained
dangling state for plugins no longer on disk.
Replace the disable with purgePluginFromSettings, which deletes both
the EnabledPlugins entry and any PluginConfigs entry for the removed
plugin, leaving unrelated plugins' state intact. A re-install defaults
to enabled=true via the existing install paths, matching user
expectations.
Covered by TestRemovePluginPurgesSettings across both user and legacy
managed layouts.
Drop the hardcoded default-managed-plugin bootstrap that auto-fetched
DingTalk-Real-AI/* plugins on CLI startup. All third-party plugins are
now installed equally via `dws plugin install` — no privileged workspace.
Motivation (issue #124): first-run startup printed
WARN failed to fetch remote info for default plugin
DingTalk-Real-AI/conference: invalid character '<' looking for
beginning of value
because the GitHub Pages registry returned HTML for the missing plugin.
Beyond the error, the whole "built-in plugin" concept contradicts the
CLI's lightweight design: plugin installation planning belongs to
agent-authored skills, not to the CLI binary.
Changes:
- Delete internal/plugin/updater.go (EnsureManaged, CheckAndUpdate,
checkRemoteVersion, downloadAndInstall, promptUpdate, zip extraction
— the entire managed-plugin update pipeline).
- Delete internal/plugin/updater_test.go.
- Remove DefaultManagedPlugins, OfficialPluginWorkspace, and
PluginUpdateCheckInterval from pkg/config/constants.go.
- Remove the EnsureManaged/CheckAndUpdate bootstrap block from
internal/app/root.go; only legacy LoadManaged() kept for back-compat
so plugins already installed under ~/.dws/plugins/managed/ still load.
- internal/plugin/loader.go: InstallFromGit always installs under
PluginUserDir; RemovePlugin allows removal of legacy managed-dir
plugins instead of refusing.
- internal/app/plugin_cmd.go: drop `--type managed` flag and related
gating from `plugin install`; disable/remove commands no longer
differentiate managed vs user.
- Update plugin_test.go accordingly (replace TestRemoveManagedPluginBlocked
with TestRemoveLegacyManagedPlugin, drop TestPromptUpdate).
Backward compatibility: `PluginManagedDir = "managed"` constant retained
so plugins already on disk from older CLI versions still load and are
removable. No migration required.
Verification:
- go build ./cmd → success
- go test ./internal/plugin/... → PASS
- go test ./internal/app/... → PASS (436s, matches main baseline)
- Smoke: `/tmp/dws-new plugin list` no longer prints the "Pulling
built-in plugin" or "failed to fetch remote info" WARN.
- Pre-existing failures on main (unrelated): test/cli_compat,
test/integration/extensions (requires auth login),
test/scripts (requires DWS_PACKAGE_VERSION or git tag),
test/unit TestOpenSourceTreeOmitsEmbeddedHostMarkers.
Closes#124
Follow-up to cache-first registration (c95ec04). The cold-cache wall
clock is now bounded by the slowest individual plugin, not the sum:
- Fan out HTTP and stdio discovery together in goroutines rather than
running the stdio loop serially after HTTP.
- HTTP cold budget: 4s → 700ms (auth) / 500ms (plain). Honest dial
timeouts on unreachable endpoints fail fast; healthy third-party
endpoints respond well under the window. The outcome is still saved
as a negative cache, so a miss this run costs the next run ~0ms.
- Stdio cold budget: 4s → 1s. A local subprocess handshake is milliseconds.
With three user plugins (one pointing at TEST-NET-1, permanently
unreachable), cold `dws --help` drops from ~3.3s to ~0.8s and warm stays
at ~80ms. The savings compound linearly with plugin count.
Follow-up to the 4s startup cap (df01f36). Plugin command registration
now reads the tools snapshot directly from the cache store on the hot
path, and only falls through to an Initialize+ListTools RPC when no
snapshot exists.
- registerHTTPServer / registerStdioServer try cache.LoadTools first;
on hit they build Cobra commands synchronously with zero network I/O.
- Cold cache falls back to the existing synchronous discovery (already
bounded at 2-4s) and persists the outcome — including empty tool
lists — as a negative cache so the next invocation is fast regardless
of endpoint health.
- Cache entries are namespaced under "plugin:<name>:<server>" so they
are distinct from Market cache entries in `dws cache status`, and
refresh on-demand via `dws cache clean` / `dws cache refresh`; the
existing 7d ToolsTTL otherwise expires entries naturally.
Warm-cache `dws --help` with three user plugins (one unreachable) now
returns in ~80ms versus 3.7s with synchronous discovery, a >40x win
when endpoints are offline.
Issue #119: an unreachable third-party MCP plugin (e.g. blocked/firewalled
endpoint) blocks `dws --help` for ~10s on every CLI invocation, because
plugin discovery happens eagerly during command-tree construction.
Root cause was a stack of timeouts that multiplied under transport failure:
* `transport.Client.Initialize` loops three supported protocol versions on
every error — including dial timeouts and HTTP 5xx — even though those
failure modes are protocol-version-independent. Three loops × the dial
budget = 3× the worst-case startup cost.
* Default `DialContext.Timeout` was 10s, so a single dial against an
unroutable address (e.g. TEST-NET-1) burned the full plugin context.
* `registerHTTPServer` granted plugins with `AuthHeaders` a 10s outer
context (intended for slow third-party services), and `registerStdioServer`
granted every stdio plugin 10s. Either single misbehaving plugin therefore
stalled the entire CLI.
* Registry-side `defaultDiscoveryTimeout` (10s) and `perServerDiscoveryTimeout`
(5s) had the same shape on the LoadCatalog path.
Changes:
* `transport.Client.Initialize`: short-circuit when the underlying
`*CallError.Stage` is anything other than `CallStageJSONRPC`. Protocol
version negotiation is the only justification for retrying with another
version; transport/HTTP failures fail identically and should surrender.
* `transport.defaultTransport`: `DialContext.Timeout` 10s → 3s.
* `app.registerHTTPServer`: AuthHeaders timeout 10s → 4s.
* `app.registerStdioServer`: outer ctx timeout 10s → 4s.
* `cli.defaultDiscoveryTimeout`: 10s → 4s.
* `discovery.Service`: rename `perServerDiscoveryTimeout` 5s → 2s
(`defaultPerServerDiscoveryTimeout`); add `Service.PerServerTimeout`
override field for tests/callers needing a tighter or looser bound.
Tests:
* `TestInitializeShortCircuitsOnHTTPError` — asserts only ONE protocol
version is attempted on HTTP 5xx.
* `TestInitializeShortCircuitsOnDialFailure` — asserts Initialize returns
in <2s on a refused-connection address.
* Existing `TestInitializeNegotiatesProtocolVersion` continues to pass —
JSON-RPC-stage errors still trigger version fallback.
End-to-end measurement with a blackhole plugin
(endpoint=192.0.2.1, AuthHeaders set):
| Variant | median `dws --help` |
| ------------- | ------------------- |
| main | 10.2s |
| PR #121 alone | 9.1s |
| this branch | 3.72s (-63%) |
The `loader.go` and `discovery/service.go` timeout reductions overlap with
PR #121 by @utafrali; see PR description for attribution.
Refs: #119
Supersedes: #121
- Validate git URL protocol: reject file:// and local paths, only allow https/ssh
- Reject symlink entries in ZIP extraction to prevent path traversal
- Validate build.output must be relative path within plugin directory
- Reject absolute paths in stdio server command declarations
- Block dangerous env var names (PATH, LD_PRELOAD, etc.) from plugin config injection
- Remove conference from default managed plugins (source not yet available)
- Add `EnsureManaged` to the plugin updater to automatically install missing default plugins on startup.
- Translate the managed plugin removal error message to English for better consistency.
- Update tests to match the new error message.
Ignore `MinCLIVersion` validation when the current CLI version is "dev". This allows plugins to be loaded during local development and testing without being blocked by strict semantic version constraints.
Prevent plugins from hijacking built-in commands (auth, plugin, cache,
etc.) and detect duplicate names between plugins.
- Add reservedCommands set for protected built-in command names
- Add addPluginCommandsSafe with three conflict rules:
- Plugin vs reserved → reject with warning
- Plugin vs plugin → first wins, second rejected with warning
- Plugin vs Market → plugin wins (intentional override)
- Fix plugin create scaffold format string arg count
Add `build` field to plugin.json so stdio servers can be automatically
compiled to native binaries during install, eliminating runtime deps.
- Add BuildConfig struct to Manifest (command + output fields)
- Add BuildPlugin/runBuild in loader with output verification
- Auto-trigger build in InstallFromDir and InstallFromGit (rollback on failure)
- Add `dws plugin build <dir>` command for manual builds
- Update `plugin create` scaffold to include build template
- Fix schema-derived flags not being collected into tool call params
- Add registerHTTPServer for plugin streamable-http tool discovery
- Include dev plugins in ListInstalled output
- Isolate settings path for test environments
- Add stdio and HTTP MCP end-to-end integration tests
Stdio MCP servers discovered by the plugin loader now have their tools
automatically registered as CLI subcommands. The runner dispatches
stdio:// virtual endpoints to the local StdioClient subprocess instead
of the HTTP transport, enabling plugin tools to be invoked directly
from the command line (e.g. `dws hello greet --name Peter`).
Also fixes a bug where plugin endpoints registered via AppendDynamicServer
were overwritten by the subsequent SetDynamicServers call in
loadDynamicCommands, and a context lifecycle bug where stdio subprocesses
were killed immediately after startup due to a short-lived timeout context.
- Wire updater into loadPlugins to check managed plugin updates on
CLI startup with 10s timeout and best-effort semantics
- Add StdioClients() to Plugin for creating stdio transport clients
with DWS_PLUGIN_ROOT/DWS_PLUGIN_DATA variable expansion
- Start and initialize stdio MCP server subprocesses during plugin load
- Reorder loadPlugins: update check → load → http inject → stdio start → hooks
- Add updater.go for managed plugin auto-update (version check, download,
Y/n prompt, zip extraction with zip-slip protection)
- Add stdio.go transport for local MCP server subprocess communication
via stdin/stdout JSON-RPC 2.0
- Implement InstallFromGit in loader.go with URL parsing for HTTPS/SSH
- Wire up --git flag in plugin install command
- Switch all plugin CLI output to English (headers, status, messages)
- Add tests for parseGitURL and promptUpdate
Introduce the plugin system for DWS CLI, enabling official and third-party
plugin management. This includes plugin manifest parsing/validation, loader
with managed/user directory-based identity, MCP server conversion and
injection into the dynamic routing registry, pipeline hook adapter for
shell-based hooks, and CLI commands (list/install/info/enable/disable/
remove/validate).