Compare commits

...
Author SHA1 Message Date
修雨 434251695c fix(release): recover stable tag sharing beta commit 2026-07-15 09:08:12 +08:00
修雨 4e59f9aa7a docs(changelog): seal v1.0.52 release notes (#619) 2026-07-14 23:04:01 +08:00
修雨 047ac54afe fix(connect): forward complex message payloads (#612)
Remove content-shape and message-type attachment filtering, recover forwarded unknown attachments, and preserve original media across all agent backends.
2026-07-14 22:31:21 +08:00
修雨 9a78a6494a Merge pull request #618 from DingTalk-Real-AI/codex/sync-wukong-im-read-results
feat(im): sync Wukong read-result semantics
2026-07-14 22:31:07 +08:00
修雨 73bf77d479 feat(im): sync Wukong read-result semantics 2026-07-14 19:04:05 +08:00
修雨 a98ae9c6cf Merge pull request #598 from typefield/feat/schema-on-main
feat(schema): add stable Agent command catalog
2026-07-14 17:26:12 +08:00
玉澜andCursor 918c73f418 docs(changelog): record stable 22-product Agent catalog for #598
Document the embedded Schema catalog delivery under Unreleased Added so
the PR documentation gate matches the shipped surface.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 17:11:44 +08:00
玉澜andCursor ef3c2feafb feat(schema): cover audit export/tail/verify from #555
Merge upstream main and publish the three public audit leaves into the
CommandRegistry, metadata/selection hints, and regenerated Catalog so
reverse completeness stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:54:29 +08:00
玉澜 0a7b6d8406 Merge upstream/main into feat/schema-on-main
Bring in #555 audit export/tail/verify public leaves for schema completeness.
2026-07-14 16:36:01 +08:00
玉澜andCursor b2561388fe fix(schema): keep Cobra hard-required as required projection floor
Stop letting manual/hint overlays project MarkFlagRequired flags as
optional; add final payload regression and gofmt the disposition test.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 15:27:23 +08:00
SCzheng da30780684 Merge pull request #555 from DingTalk-Real-AI/feat/audit-log-v2
feat(audit): implement user operation audit log
2026-07-14 14:54:44 +08:00
修雨 96986dfbff style(audit): gofmt trailing newline in audit_runtime_test.go
Fixes the Lint (Format Check) CI failure introduced by the previous
commit; gofmt flagged a trailing blank line at EOF.
2026-07-14 14:35:29 +08:00
修雨 27c3449036 fix(audit): drain forwards on error exit, fail CSV on corrupt JSONL
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).
2026-07-14 14:22:13 +08:00
玉澜andCursor e9cd8c9ad9 fix(schema): align event.stop confirmation with runtime --yes gate
Catalog safety now matches the existing CLI confirmation requirement so
Agent metadata and TestEventRegistry stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 14:16:12 +08:00
玉澜andCursor 5856a897d1 feat(schema): split human hints into metadata and selection
Own safety/gates/parameters in metadata/ and Agent prose in selection/,
drop the monolithic Manual file, and keep confirmation aligned with
per-tool runtime_gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:40:09 +08:00
修雨 d0787ce8ee fix(audit): stateless hash chain, waitable forwarder, profile actor, observability
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
2026-07-14 11:56:09 +08:00
玉澜andCursor 363ca3de9b feat(schema): curate agent selection hints from live MCP and runtime gates
Rewrite use_when/avoid_when/examples with live dws schema plus Skill/Cobra
review, expand runtime_gates to 70 confirmed commands, and regenerate catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 11:37:47 +08:00
玉澜andCursor 987273f32b feat(schema): align confirmation with runtime via index+products hints
Make agent hints authoritative for confirmation by loading
internal/cli/schema_hints/index.json + products/*, and gate catalog
user_required to the reviewed runtime_gates set.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 10:33:49 +08:00
修雨 32c1d772de fix(root): expose audit command in help and reserve it from plugins 2026-07-14 10:30:05 +08:00
修雨 39b3003e2f Merge branch 'main' into feat/audit-log-v2 2026-07-14 10:21:15 +08:00
玉澜 f423031074 ci: allow full schema race gate to finish 2026-07-14 08:24:41 +08:00
玉澜 2879683cad fix(schema): enforce final delivery and runtime contracts 2026-07-14 08:11:27 +08:00
玉澜 878bafe55d perf(schema): keep delivery gates within race budget 2026-07-14 01:56:03 +08:00
玉澜 114c47b62d test(event): align restart hint with safe stop flow 2026-07-14 01:30:59 +08:00
玉澜 6d97bf7204 Merge upstream/main into feat/schema-on-main
Complete the registry-first Schema delivery invariants, bind the event command surface, and preserve the event subprocess contract from main.
2026-07-14 01:25:05 +08:00
玉澜 06cea56e92 fix(schema): close resolver and runtime contract gaps 2026-07-14 00:06:37 +08:00
玉澜 1f2b992e9c fix(schema): align capability contracts and delivery 2026-07-13 22:51:38 +08:00
玉澜 fc415919d1 fix(ci): remove ripgrep dependency from schema policy 2026-07-13 21:15:56 +08:00
玉澜 125f0c8fe0 Merge remote-tracking branch 'upstream/main' into feat/schema-on-main
# Conflicts:
#	test/scripts/package_script_test.go
2026-07-13 21:09:27 +08:00
玉澜 55afc656ac fix(schema): validate agent example delivery 2026-07-13 20:33:56 +08:00
玉澜 f6c2ce655d refactor(schema): unify registry-first delivery 2026-07-13 19:49:40 +08:00
玉澜 d41ea586bf fix(schema): enforce catalog and interface completeness 2026-07-13 14:01:26 +08:00
玉澜 f77232d7c1 feat(schema): add agent-friendly manual hints 2026-07-13 13:41:30 +08:00
玉澜 31faf7205b docs: add repository agent guidance 2026-07-13 11:53:18 +08:00
玉澜 45e0423d46 fix(schema): enforce command and safety completeness 2026-07-13 11:50:20 +08:00
玉澜 1b70d8f3f2 Merge remote-tracking branch 'upstream/main' into feat/schema-on-main 2026-07-13 11:05:19 +08:00
玉澜 a6f309d011 fix(schema): lazily load embedded catalog 2026-07-13 11:05:08 +08:00
玉澜 a6b2972a1e feat(schema): review sheet range and filter agent semantics
Add explicit reviewed Agent hints for high-frequency sheet range/filter/filter-view, condition-format and dropdown tools. Replace generic avoid_when with concrete read/write/clear/style/filter-view disambiguation, tighten destructive operations, and regenerate schema metadata/catalog.

Validated with drift/catalog gates and go test ./internal/cli ./internal/app ./internal/generator/... .
2026-07-11 17:39:57 +08:00
玉澜 1e0a171ceb feat(schema): review attendance agent semantics
Add explicit attendance Agent review hints for all 38 attendance tools, replacing template avoid_when with business-specific selection guidance and marking them reviewed. Tighten high-impact attendance writes such as boss-check and settings/balance updates with high risk and user confirmation.

Regenerate schema metadata/catalog and update parameter binding hash. Drift/catalog gates and key schema tests pass.
2026-07-11 17:32:39 +08:00
玉澜 cb4d1c215c feat(schema): generate catalog from live Cobra tree without fallback
Stop registering runtime catalog fallback commands and make command-surface generation use the real Cobra tree directly. Regenerate schema surface, agent metadata and catalog from executable commands (20 products / 537 tools), add runtime-surface completeness hints, and update catalog gates/tests to use dynamic counts instead of old 504/21/461 constants.

This makes schema describe the actual executable CLI surface; drift/catalog gates and go test ./internal/cli ./internal/app ./internal/generator/... pass.
2026-07-11 16:07:23 +08:00
玉澜 538754bbba feat(schema): sharpen aitable view summaries and sibling disambiguation
Add explicit reviewed summaries for aitable view get/update subcommands so Agents
can distinguish filter, sort, group, visible-fields, aggregate, card and other
view operations. Regenerate sibling-disambiguation avoid_when entries from the
new summaries, making cross-tool guidance precise instead of generic.

Results: 395/504 tools carry sibling-command disambiguation and reviewed coverage
rises to 104/504. drift/catalog gates and go test ./internal/cli pass.
2026-07-11 14:31:38 +08:00
玉澜 c2010b912b chore(schema): drop accidentally committed dwsbin binary and ignore it 2026-07-11 14:03:10 +08:00
玉澜 cf8cf95087 feat(schema): add sibling-command disambiguation to avoid_when
Add skills/mono/schema-hints/sibling-disambiguation.json: for each multi-segment
command sub-group (aitable view update, sheet range, chat message, ...), append
explicit cross-referencing avoid_when entries pointing agents to the correct
sibling command. Regenerate embedded agent metadata + catalog: 395/504 tools now
carry sibling disambiguation, improving tool-selection beyond template-only
avoid_when. drift/catalog gates and go test ./internal/cli pass.
2026-07-11 14:02:05 +08:00
玉澜 f86d10ae63 feat(schema): add --compact mode and update SKILL.md schema guide
- Add --compact flag to schema command (canonical.go)
- Implement stripSchemaPayloadCompact to recursively remove provenance/
  debug/redundant fields (runtime_schema.go)
- Strip 27 top-level keys (agent_metadata_source, agent_source_refs,
  interface_ref, primary_cli_path, etc.) and 3 per-parameter keys
  (interface_description, interface_type, property)
- Add 3 tests covering leaf/overview/product compact modes
- Replace stale SKILL.md schema section with progressive query guide,
  compact field reference, and schema-vs-help decision table
- Regenerate schema artifacts (make generate-schema)

Size reduction:
  leaf: 9.5KB -> 6.0KB (36%)
  --all: 644KB -> 414KB (36%)
2026-07-11 13:41:19 +08:00
玉澜 3f3ece933b feat(schema): complete reviewed agent metadata 2026-07-11 12:23:55 +08:00
玉澜 3fac462410 fix(schema): keep defaulted pagination optional 2026-07-11 11:42:06 +08:00
玉澜 753866867f feat(schema): complete catalog contract and smoke gates 2026-07-11 11:21:44 +08:00
玉澜 a62bcdf460 fix(schema): audit fallback parameter bindings 2026-07-11 10:42:02 +08:00
玉澜 561525a18b feat(schema): generate stable agent command catalog 2026-07-11 10:28:10 +08:00
玉澜 e1ea573247 feat(schema): align dws schema with prior branch and GWS/Lark contract
Serve the versioned embedded Command Catalog (21 products / 504 tools) from
NewSchemaCommand instead of only the live tree, matching the prior branch's
release behavior and the GWS flat-leaf / Lark stable-canonical contract. Add
--all and route output through internal/output for --format/--jq/--fields.
Port schema_catalog_test.go asserting 504/21 embedded catalog integrity.
Helper subtree and live Cobra tree remain as fallbacks.
2026-07-11 01:58:36 +08:00
玉澜 eb9e6be944 merge feat/schema-gws-flat into upstream static-endpoint schema branch
Consolidate the prior schema branch (old discovery-based architecture) into the
upstream-based dynamic-schema implementation. Merged tree keeps the upstream
static-endpoint architecture with dynamic schema; old discovery/generator/compat
packages are not carried over (incompatible with upstream, superseded by the
live-tree dynamic schema). Old schema data assets (agent metadata, destructive
safety annotations, conference metadata) remain present via the ported runtime.

Brings origin/feat/schema-gws-flat history in, so pushing is a fast-forward.
2026-07-11 01:46:18 +08:00
玉澜 ec59f7b042 feat(schema): implement dynamic schema on static-endpoint architecture
Restore dynamic dws schema on top of upstream static-endpoint runtime
(v1.0.52) without re-introducing service discovery:
- port schema runtime (runtime_schema/schema_catalog/schema_agent_metadata/
  schema_hints) + embedded agent & interface metadata + ir data structures
- ir/catalog.go: drop discovery-dependent BuildCatalog, keep runtime types
- canonical.go NewSchemaCommand: build schema from the live Cobra tree via
  runtimeSchemaPayload instead of the stub
- add schema_support.go and design doc docs/schema-dynamic-endpoint-design.md

go build ./... passes; go test ./... 44 packages pass (only unrelated
post-goreleaser packaging tests fail with a known tar format issue).
2026-07-11 01:26:22 +08:00
玉澜 63c0b26cf6 feat: add conference agent metadata (summary/effect/reviewed)
Add skills/mono/schema-hints/conference.json annotating all 33 conference
meeting-control tools with agent_summary, effect and reviewed=true. Mark
end-meeting-for-all as risk=high + confirmation=user_required; mute-all and
cloud-record start/stop as risk=medium.

Coverage: missing agent_summary 81->48, missing effect 173->140,
reviewed=true 4->37. Drift/catalog gates, go test and 560-case smoke pass.
2026-07-11 00:04:54 +08:00
玉澜 5004fcd285 feat: add destructive-operation safety metadata to agent schema
Annotate 34 high-risk tools via skills/mono/schema-hints/destructive-safety.json
(30 destructive + 4 disable) with risk=high and confirmation=user_required, and
fix mergeToolMetadata effect precedence (effectSourceRank) so explicit hints
override command-verb inference. Regenerate embedded agent metadata and catalog.

risk=high coverage 22->56, effect=destructive 29->48; drift/catalog gates,
go test, and 560-case schema smoke all pass.
2026-07-10 23:50:53 +08:00
玉澜 eec64bdf35 fix: align schema aliases and one-of coverage 2026-07-10 17:13:29 +08:00
玉澜 ddad2f648c fix: align agent schema parameter contracts 2026-07-10 15:12:49 +08:00
玉澜 391e761b59 fix: stabilize agent schema metadata 2026-07-10 13:42:10 +08:00
玉澜 a15fb19fd2 test: retire obsolete discovery compatibility suite 2026-07-10 13:06:24 +08:00
玉澜 70107e008f feat: embed agent-optimized schema metadata 2026-07-10 12:53:51 +08:00
玉澜 15e0851e06 fix: cover attendance schema smoke cases 2026-07-09 16:11:10 +08:00
玉澜 f1ca55c649 fix: make schema smoke mail search deterministic 2026-07-09 13:52:08 +08:00
玉澜 4440479c5c feat: align runtime schema smoke validation 2026-07-09 11:32:25 +08:00
修雨 5ac5fcbf16 Merge remote-tracking branch 'origin/main' into feat/audit-log-v2
# Conflicts:
#	internal/helpers/devapp_connect.go
2026-07-08 13:59:54 +08:00
修雨 4a717bd92f feat(audit): implement user operation audit log
- Add internal/audit package: Event struct, FileSink, date rotation, L1 hash chain, HTTP forwarding, 3-tier redaction
- Integrate with runner: emit audit event in executeInvocation defer
- Add dws audit tail/export/verify command group
- Register DWS_AUDIT* env vars in configmeta, enabled by default
2026-07-06 11:55:40 +08:00
玉澜 604ec5f50a Merge remote-tracking branch 'origin/feat/dws-event' into feat/dws-event 2026-07-06 10:04:19 +08:00
玉澜 27296ec426 fix: remove subscribe id event fanout filter 2026-07-06 10:04:12 +08:00
wxianfeng 6a38a168dd install script event 2026-07-02 20:41:46 +08:00
wxianfeng 9771053d81 default value 2026-07-02 20:14:30 +08:00
wxianfeng 10c0c5083e event skill 2026-07-02 19:42:10 +08:00
wxianfeng a0187b5297 Merge branch 'feat/dws-event' of github.com:wxianfeng/dingtalk-workspace-cli into feat/dws-event 2026-07-02 17:15:48 +08:00
wxianfeng 81991f1c07 opt 2026-07-02 17:14:55 +08:00
xianfeng wang 836670ef50 Merge pull request #24 from sczheng189/feat/dws-event
fix(event): unix socket 路径超长时 fallback 到短路径,修复深层配置目录下 bus 无法启动
2026-07-02 16:54:58 +08:00
zhengyubai 53ce0a8303 refactor(event): 优化IPC端点路径处理和改进相关测试
- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖
2026-07-02 17:25:56 +09:00
wxianfeng 78867f3601 eventType filter 2026-07-02 16:19:28 +08:00
wxianfeng 37438659e6 user event 2026-07-01 15:58:09 +08:00
wxianfeng 3c12c835a3 Merge branch 'feat/dws-event' of github.com:wxianfeng/dingtalk-workspace-cli into feat/dws-event 2026-07-01 14:22:06 +08:00
wxianfeng 389f83241f event 2026-07-01 14:21:36 +08:00
玉澜 3714adc2db Merge remote-tracking branch 'origin/feat/dws-event' into feat/dws-event
# Conflicts:
#	internal/app/event_command.go
2026-07-01 14:20:17 +08:00
玉澜 f37d0569a1 fix: allow portal ticket normal without app secret 2026-07-01 14:17:12 +08:00
wxianfeng 798b58bf3c fix conflict 2026-07-01 11:15:48 +08:00
wxianfeng d926bed3cc user event 2026-07-01 11:07:57 +08:00
玉澜 5ac180d3dd feat: add portal ticket stream mode 2026-06-30 20:38:27 +08:00
玉澜 35e60407d3 test: add stream ticket injection probe 2026-06-30 15:33:17 +08:00
wxianfeng ea46132cf6 merge upstream main 2026-06-29 16:15:13 +08:00
wxianfeng 478dc155e8 fix consume fail 2026-06-04 10:41:50 +08:00
wxianfeng 08ecb38a42 dws event 2026-06-03 19:12:23 +08:00
352 changed files with 684654 additions and 2061 deletions
+6 -1
View File
@@ -59,7 +59,12 @@ jobs:
run: make build
- name: Test with Race Detection
run: go test -v -race -count=1 -timeout=5m ./cmd/... ./internal/...
# The registry-first final-delivery gate validates all public commands
# and the complete generated Catalog under the race detector. Keep the
# package timeout aligned with the macOS race job so the Linux runner's
# five-minute default does not expire while that gate is still making
# progress.
run: go test -v -race -count=1 -timeout=10m ./cmd/... ./internal/...
- name: Test release scripts
run: go test -v -count=1 -timeout=5m ./test/scripts
+53 -11
View File
@@ -10,13 +10,24 @@ on:
description: "Only publish an existing release to npm, e.g. v1.0.48"
required: false
type: string
recover_release_version:
description: "Recover an existing undelivered tag, e.g. v1.0.52"
required: false
type: string
recover_release_previous_tag:
description: "Previous delivered tag used for recovery changelog generation"
required: false
type: string
permissions:
contents: write
env:
RELEASE_VERSION: ${{ inputs.recover_release_version || github.ref_name }}
jobs:
release:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.recover_release_version != '') }}
runs-on: ubuntu-latest
timeout-minutes: 30
@@ -25,6 +36,35 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.RELEASE_VERSION }}
- name: Validate undelivered recovery target
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PREVIOUS_TAG: ${{ inputs.recover_release_previous_tag }}
run: |
set -euo pipefail
case "$RELEASE_VERSION" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Recovery requires a stable vX.Y.Z tag" >&2; exit 1 ;;
esac
case "$RELEASE_VERSION" in
*-*) echo "Stable recovery cannot use a prerelease tag" >&2; exit 1 ;;
esac
test -n "$PREVIOUS_TAG"
test "$(git cat-file -t "refs/tags/$RELEASE_VERSION")" = tag
target_commit="$(git rev-parse "$RELEASE_VERSION^{commit}")"
test "$target_commit" = "$(git rev-parse HEAD)"
git fetch --force origin main --tags
test "$target_commit" = "$(git rev-parse origin/main)"
git rev-parse "$PREVIOUS_TAG^{commit}" >/dev/null
if gh release view "$RELEASE_VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Recovery target already has a GitHub Release" >&2
exit 1
fi
echo "GORELEASER_CURRENT_TAG=$RELEASE_VERSION" >> "$GITHUB_ENV"
echo "GORELEASER_PREVIOUS_TAG=$PREVIOUS_TAG" >> "$GITHUB_ENV"
- name: Set up Go
uses: actions/setup-go@v5
@@ -92,7 +132,7 @@ jobs:
- name: Post-release packaging
run: ./scripts/release/post-goreleaser.sh
env:
DWS_PACKAGE_VERSION: ${{ github.ref_name }}
DWS_PACKAGE_VERSION: ${{ env.RELEASE_VERSION }}
DWS_REQUIRE_DEVELOPER_ID_SIGNING: ${{ github.repository_owner == 'DingTalk-Real-AI' }}
- name: Remove Apple Developer ID certificate
@@ -108,7 +148,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DWS_PUBLISH_RELEASE: "false"
run: ./scripts/release/finalize-github-release.sh
run: GITHUB_REF_NAME="$RELEASE_VERSION" ./scripts/release/finalize-github-release.sh
- name: Preserve finalized distribution files
uses: actions/upload-artifact@v4
@@ -119,7 +159,7 @@ jobs:
retention-days: 1
verify-darwin-signatures:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.recover_release_version != '') }}
needs: release
runs-on: macos-latest
timeout-minutes: 10
@@ -131,7 +171,7 @@ jobs:
run: |
set -euo pipefail
mkdir -p dist
gh release download "$GITHUB_REF_NAME" \
gh release download "$RELEASE_VERSION" \
--repo "$GITHUB_REPOSITORY" \
--dir dist \
--pattern 'dws-darwin-amd64.tar.gz' \
@@ -152,7 +192,7 @@ jobs:
done
publish-release:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.recover_release_version != '') }}
needs:
- release
- verify-darwin-signatures
@@ -164,6 +204,8 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_VERSION }}
- name: Restore finalized distribution files
uses: actions/download-artifact@v4
@@ -174,14 +216,14 @@ jobs:
- name: Publish verified Draft release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false
run: gh release edit "$RELEASE_VERSION" --repo "$GITHUB_REPOSITORY" --draft=false
- name: Sync release to China OSS mirror
# 自动同步到国内镜像,供 install.sh 的 DWS_RELEASE_BASE 开关消费。
# 脚本自带门控:未配置 OSS_* secret 时优雅跳过,不影响海外发布。
run: ./scripts/release/sync-to-oss.sh
env:
VERSION: ${{ github.ref_name }}
VERSION: ${{ env.RELEASE_VERSION }}
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
@@ -197,7 +239,7 @@ jobs:
- name: Publish stable to npm
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉。
# 必须在 Gitee mirror 前发布:Gitee 附件上传偶发长时间挂住,不能阻塞 npm/latest。
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }}
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(env.RELEASE_VERSION, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public
env:
@@ -205,7 +247,7 @@ jobs:
- name: Publish prerelease to npm beta
# 预发布版本不能更新 npm latest,避免普通 npm 安装链路拿到 beta。
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(github.ref_name, '-') }}
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(env.RELEASE_VERSION, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public --tag beta
env:
@@ -220,7 +262,7 @@ jobs:
timeout-minutes: 20
run: ./scripts/release/sync-to-gitee.sh
env:
VERSION: ${{ github.ref_name }}
VERSION: ${{ env.RELEASE_VERSION }}
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_USER: ${{ secrets.GITEE_USER }}
GITEE_REPO: ${{ secrets.GITEE_REPO }}
+1
View File
@@ -47,3 +47,4 @@ test/dev_functional/results.jsonl
/.qoder/
.vercel
.env*
dwsbin
+357
View File
@@ -0,0 +1,357 @@
# Repository Agent Guide
This file applies to the entire repository. Keep changes scoped, preserve
unrelated work, and use `gofmt` for every modified Go file.
## Build and test
- Build: `go build ./cmd`
- Full test suite: `DWS_PACKAGE_VERSION=0.0.0-test go test ./...`
- Generate Schema assets: `go generate ./internal/cli`
- Check generated drift: `./scripts/policy/check-generated-drift.sh`
- Check the Schema contract: `./scripts/policy/check-schema-catalog.sh`
Generated Schema JSON is committed. Change its source inputs and generators,
then regenerate; do not hand-edit generated Catalog or Agent metadata files.
`internal/cli/schema_command_registry.json` is different: it is a reviewed
`CommandRegistry` source, not a generated snapshot. It is the single reviewed
source of stable canonical identity,
primary paths, aliases, and navigation. Edit it only when reviewed exposure,
identity, primary path, or aliases change; parameter, Skill, and metadata-only
changes must not rewrite it mechanically.
## Agent Schema contract
The Schema data flow is one way:
```text
1. app.NewRootCommand()
└─ builds the real Cobra command tree and flags
2. schema_command_registry.json
+ schema_hints/metadata/<product>.json tool parameters (+ cli_path)
└─ forms EffectiveCommandRegistry
└─ binds exactly to real Cobra leaves and aliases
3. Parameter resolution
Cobra flags
+ schema_parameter_bindings.json
+ metadata tool parameters
└─ produces ParameterSpec and constraints
4. Agent and interface semantics
schema_hints/selection/<product>.json (selection prose)
+ schema_hints/metadata/<product>.json (safety/interface/runtime_gate)
+ pinned MCP metadata
└─ resolves Agent metadata by source precedence
Markdown is evidence only; it is not concatenated into final prose
5. One typed hub
BoundCommandRegistry
+ ParameterSpec
+ Agent metadata
+ Interface metadata
└─ resolves every command exactly once into ToolSpec
└─ aggregates SchemaRegistry + SchemaIndex
6. One-way publication
SchemaRegistry
└─ internal/cli/schema_catalog.json
└─ dws schema list/product/group/leaf/--all
```
Parameter overlays from metadata are merged into `EffectiveCommandRegistry`
*before* Cobra binding; after that point there is no second identity source and
no identity precedence winner. The binder must reject a missing/non-runnable
Cobra path, an alias collision, and any native identity annotation that
disagrees with the effective registry. A missing native identity annotation is
allowed because annotations are implementation-side assertions, not identity
fallbacks.
The assembler resolves every bound command exactly once into one `ToolSpec`.
Build-time gates and the snapshot serializer consume that source-resolved typed
registry/index. Runtime projections and delivery gates consume the typed
registry/index returned by the production snapshot loader. Neither path may
reopen annotations, merge source records, or use a previous Catalog or other
generated JSON as a source. `schema_catalog.json` is output-only in the
generation graph. The production loader decoding the embedded published
snapshot is a delivery boundary, not source resolution; it must never create or
repair a Cobra command, flag, registry entry, or later Catalog generation.
This split is architecturally isomorphic to Lark's typed metadata registry,
navigation catalog, and schema renderer. DWS intentionally preserves its
existing flat JSON wire contract for compatibility; do not treat architectural
alignment as permission to make an unversioned wire-format change.
The reviewed `CommandRegistry` is the sole source of stable command identity
and navigation. The executable Cobra tree remains the source of truth for
whether a CLI path exists, is runnable, and which flags it accepts. Schema
coverage is bidirectional:
1. Every final `SchemaRegistry` tool, including its serialized Catalog
projection, must resolve to an executable Cobra command.
2. Every public runnable Cobra leaf must either resolve to Schema or appear as
an exact, reviewed exclusion with a non-empty reason in
`internal/cli/schema_command_exclusions.json`.
Do not use prefix or wildcard exclusions: they can silently hide future
commands. Remove an exclusion when its command enters Schema; stale, invalid,
or duplicate exclusions must fail generation and CI.
When adding or changing an Agent-visible command, review all relevant inputs:
- `internal/cli/schema_command_registry.json` for the reviewed
`CommandRegistry`: canonical identity, primary CLI path, aliases, and stable
navigation. It is the identity source and is not a generated artifact.
- `internal/cli/schema_command_registry.schema.json` is its closed,
machine-readable editing contract. Preserve the local `$schema` reference;
unknown fields, invalid visibility values, stale paths, and collisions fail
Go validation and policy.
- `internal/cli/schema_hints/metadata/<product>.json` for safety, interface,
`runtime_gate`, and optional parameter overlays (`parameters` / `cli_path`).
- `internal/cli/schema_hints/selection/<product>.json` for reviewed Agent
selection prose (`agent_summary`, `use_when`, `avoid_when`, `examples`).
- `internal/cli/schema_hints/index.json` only maps product IDs to those files.
- Native Runtime Schema identity annotations, when present, as consistency
assertions against `EffectiveCommandRegistry`. They must agree exactly and
must never materialize, infer, or override registry identity.
- Flag-to-interface property mappings and required/default semantics.
- Generated files under `internal/cli/schema_agent_metadata/` and
`internal/cli/schema_catalog.json` after running generation.
Run the reverse-completeness tests whenever the Cobra tree changes. A command
that works through `dws <path>` but cannot be found through the matching
`dws schema` lookup is a contract failure unless it has a reviewed exact
exclusion.
Metadata parameter overlays must reference an exact public runnable Cobra leaf
and real flags. They may override Schema description, interface-property/type
mapping, `required`, and `required_when`; they must not create commands or
flags, define an interface, or advertise an unknown RPC. Every authored entry
requires `reviewed: true` and a non-empty review reason.
For Agent-authored metadata or selection edits:
1. Confirm the exact command and flag names in the current Cobra tree.
2. Edit only the owning block (`metadata/` or `selection/`); do not mix fields.
3. Add the smallest possible entry; do not copy generated Catalog fields into
the input.
4. Describe user-visible semantics in `review_reason` and parameter
descriptions.
5. Run generation, drift, Schema policy, and the focused CLI tests before
proposing the change.
## Agent curation workflow (Schema hints)
Use this workflow when refreshing Agent selection prose and confirmation
alignment. Prefer **agent-authored review** over bulk merge scripts that dump
`selection-review.json` or Skill Markdown into Catalog fields.
Human-authored inputs are split into two blocks:
| Block | Path | Owns |
|---|---|---|
| **metadata** | `internal/cli/schema_hints/metadata/<product>.json` | `effect` / `risk` / `confirmation` / `idempotency` / `interface_*` / `runtime_gate` / optional `parameters` |
| **selection** | `internal/cli/schema_hints/selection/<product>.json` | `agent_summary` / `use_when` / `avoid_when` / `examples` (+ product routing) |
`index.json` only maps product IDs to those files. Do not mix selection fields
into metadata files or metadata fields into selection files.
### Goals
1. **Selection prose** is decision-oriented (Feishu/Lark style): trigger intent,
sibling-command routing, and outcome shape — not a restatement of the
summary. Delivered Catalog provenance is `reviewed_explicit` from
`selection/`.
2. **Safety** follows Runtime: `confirmation=user_required` iff the tool's
metadata `runtime_gate != none` (for example `confirm_delete`, `typed_yes`,
`confirm_dangerous`).
3. **Parameter overrides** (former Manual `commands`) live on metadata tools as
`parameters` (+ `cli_path`) and are applied into EffectiveCommandRegistry.
### Authoring
For every curated tool:
1. Edit `metadata/<product>.json` for safety/interface/gates/parameters.
2. Edit `selection/<product>.json` for selection prose (`reviewed: true`,
`review_reason`, `source_refs`).
3. Run `make generate-schema`. Do not hand-edit generated
`schema_agent_metadata/` or `schema_catalog.json`.
### Pull live MCP descriptions (personal token)
Pinned `internal/cli/schema_mcp_metadata.json` is a sanitized baseline. Prefer
live Schema from a logged-in personal session:
```bash
dws auth status # token_valid should be true
dws cache refresh # refresh discovery / tools cache
dws schema <mcp-canonical> -f json
# or CLI path: dws schema --cli-path "drive copy" -f json
```
Resolve MCP identity via `interface_ref` when CLI canonical ≠ MCP path
(example: CLI `drive.copy_document` → live `doc.copy_document`). On pull
failure, fall back to Skill + Cobra Help + pinned MCP, and record evidence
(for example `live-dws-schema:<path>#FAILED`). Never print or commit tokens.
Precedence when sources disagree: **Runtime/Cobra > live MCP > pinned MCP >
Skill (evidence only)**.
### Parallel product agents
Split work by product groups. Each agent must:
- Read Skill, Cobra/`--help`, Runtime confirmation sites, and live `dws schema`
for its tools.
- Hand-write selection + metadata; forbid wholesale JSON merges from review
dumps.
- Edit only its `metadata/<product>.json` and `selection/<product>.json`.
- **Never** `git checkout` unrelated product files to “clean scope”.
### Regenerate and gates
```bash
make generate-schema
./scripts/policy/check-runtime-confirmation-truth.sh
go test ./internal/app -run '^TestSheetFinalSchemaConfirmationMatchesRuntimeGuards$' -count=1
```
Example rules (fail generation otherwise):
- At most two examples per tool; no `--yes` in stored examples.
- Examples must match live Cobra argv (path, flags, required groups).
- No shell comments in examples.
After generation, spot-check Catalog: selection provenance is
`reviewed_explicit` from `selection/`, and `user_required` count equals
metadata `runtime_gate != none`.
`make generate-schema` is a full deterministic snapshot rebuild, not an
incremental patch over the previous Catalog. It rereads every reviewed input,
removes stale generated product metadata, and rewrites the exact metadata and
Catalog projections. Incremental work happens only when an Agent or human
edits selected `metadata/` or `selection/` entries; the next publication still
recomputes all outputs. Generated files must never be read back as merge input,
and byte guards fail generation if it changes the hint inputs or CommandRegistry.
Selection prose may choose a more or less restrictive recommendation. It cannot
create a Cobra command or flag, change parameter facts, invent an
RPC/interface, alter safety metadata, or bypass command completeness. Examples
must use an executable primary/alias path and flags accepted by the live Cobra
command; never add `--yes` to stored examples.
Every example is always checked against its real `BoundCommand`: exact path,
accepted flags, Cobra required flags/positionals, and the effective
`require_one_of`, `require_together`, and `mutually_exclusive` constraints must
all pass before execution eligibility is considered. A missing required value,
constraint failure, runtime error, or MCP resolution error is a contract bug;
none is a valid reason to skip an example.
Example execution defaults to contract validation only. Runtime execution is
opt-in: an example enters `dry_run` only when its final `ToolSpec` publishes an
explicit reviewed dry-run capability. The test never injects `--yes`, and
`risk`/`confirmation` values do not manufacture preview support. A narrow
runtime precondition that cannot be derived from the typed contract may use an
exact zero-based `example_dispositions` entry with `mode=contract_only`,
`reviewed=true`, one of the schema-enumerated reason codes, and a concrete
non-empty reason. Such a disposition may only narrow an explicit dry-run
capability; it cannot turn an ordinary contract-only example into a skip.
Duplicate, missing, and out-of-range indexes fail validation. Never catch a
dry-run failure and dynamically downgrade it to `contract_only`.
Normal Go tests run the exhaustive contract gate. Run
`make test-schema-agent-examples` to additionally execute the eligible subset
through the real Cobra `--dry-run` path with isolated HOME and blocked proxies.
The test reports stable `total`, `contract`, `dry_run`, `contract_only`,
`reviewed_manual`, and per-reason counts; changing those counts requires a
review of the corresponding typed dry-run capability or manual disposition.
This target is also part of `make policy`.
Treat every tool `use_when` entry as a reviewed positive selection scenario
whose expected result is that tool's canonical path, and every `avoid_when`
entry as a reviewed negative scenario that must not choose that tool. The
deterministic gate derives a typed evaluation fixture from these same fields;
it requires exact tool coverage, a real runnable `BoundCommandRegistry`
primary command, at least one positive and negative assertion per tool, and no
literal contradictory expectations. It does not claim that string matching
proves natural-language understanding.
Semantic selection is an explicit opt-in live-model check. Run the smoke set
(one positive and one negative scenario per product) with
`DWS_AGENT_SELECTION_LIVE=1 ARK_API_KEY=... ARK_BASE_URL=... ARK_MODEL=... go test ./internal/app -run TestManualAgentSelectionArkLive -count=1`.
Add `DWS_AGENT_SELECTION_FULL=1` to evaluate every committed tool scenario, or
set `DWS_AGENT_SELECTION_CASES` to comma-separated fixture case IDs. Normal CI
never calls a model; its blockers remain the reproducible fixture, binding,
example, provenance, and final-delivery facts.
The live evaluator sends only case IDs/scenarios plus one same-product
candidate table; expected/forbidden assertions stay local and must never be
included in the model prompt. Built-in Ark HTTPS bases are allowlisted. A
different HTTPS provider requires its exact base in
`DWS_AGENT_SELECTION_ALLOWED_BASE_URLS`; plaintext HTTP is accepted only for a
loopback test server so API credentials are never sent to an arbitrary clear
text endpoint.
## Safety metadata
Parameter and safety resolution is mostly source-precedence based and
value-neutral: do not choose a winner because one value looks stricter. A
higher-priority reviewed metadata/explicit source may intentionally raise or
lower description, mapping, `effect`, `risk`, `confirmation`, or `idempotency`.
Preserve all candidates and the selected source in provenance, and fail
same-precedence conflicts rather than silently merging them.
`required` is the exception. Cobra `MarkFlagRequired` is a hard floor: the
final Agent projection must keep `required=true` and cannot be lowered by
manual/hint overlays. Overlays may still raise an optional flag to required.
`cli_required` continues to mirror the executable Cobra marker.
For command text, reviewed `ToolSchemaHint` wins first, then command-specific
Cobra Help, then MCP metadata. Generic RPC prose may remain an unselected
provenance candidate (and parameter-level `interface_description`); it must not
overwrite a specialized leaf's title or description.
For every delivered `ToolSpec` and `ParameterSpec` field, the provenance
winner value must exactly equal the delivered value. Checking only source,
count, presence, or hash is not a sufficient final-delivery invariant.
The same resolved `ToolSpec` must drive every projection. The full leaf payload
must equal the corresponding tool in `schema --all` and the full Catalog tool.
Overview/product/group summaries and Catalog summaries must equal
`ToolSpec.ToSummaryPayload()`. An alias lookup may change only the view fields
`cli_path` and `is_alias`; it must not re-resolve or mutate the command
contract.
This build-time rule is distinct from runtime drift handling. If shipped Help
and leaf Schema disagree, pass only flags accepted by Cobra. For conflicting
safety information, do not silently take the less restrictive behavior: use
the safer interpretation or stop and report the contract drift.
Do not infer one safety field from another. In particular, `effect=destructive`
or `risk=high` does not mechanically rewrite `confirmation`; the final
precedence winner for each field is authoritative. When
`confirmation=user_required`, obtain confirmation before adding `--yes`.
Keep CLI confirmation behavior and Schema metadata consistent, and add a
semantic regression test through the final embedded loader/query delivery
path; a generator unit test or JSON count alone is insufficient.
## Current Schema boundaries
- `schema list` remains a progressive overview. `schema --all` is the stable
full-export contract: every final `SchemaIndex` tool must contain its
complete leaf parameters, constraints, and safety semantics, including an empty
`parameters` object for commands without flags. Keep it suitable for the #602
compatibility baseline and fail rather than silently emitting a partial
export.
- `schema --all` is not normal command discovery. Use overview -> product/group
-> leaf for routine Agent work. `--compact` is supported for context-saving
projections, but a compact full export is not a complete compatibility
baseline.
- `dws <path> --help` defines whether Cobra exposes a path and which flags the
executable accepts. A leaf Schema defines Agent selection, parameter mapping
and constraints, and safety/confirmation semantics. A conflict is contract
drift, not permission to guess.
- Schema and Help describe commands; neither returns DingTalk business data.
After discovery, execute the real read/search/list command to obtain data.
+18 -5
View File
@@ -6,18 +6,31 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
### Changed
## [1.0.52] - 2026-07-14
- **`event consume` AI-subprocess contract** — aligns personal event streaming with the contract an orchestrator can drive without guessing: a fixed stderr ready line `[event] ready event_key=<key> bus_pid=<pid>` (block on it, don't `sleep`); a final `[event] exited — received N event(s) in Xs (reason: limit|timeout|signal|bus_shutdown)` line with exit code 0 on controlled exit and non-zero (no `exited` line) on failure; stdin-EOF as a graceful shutdown signal, armed only for a parent-controlled pipe stdin on an unbounded run (an interactive TTY and `< /dev/null` never trigger it), with a self-explaining diagnostic when it fires; and ownership-based subscription cleanup — a subscription this run created is unsubscribed on any clean exit while a `--subscribe-id`-reused one is left intact (`--ephemeral` still forces cleanup), so `kill -9` is the only way to leak a server-side subscription. Skill docs (mono + `dingtalk-event`) document the contract; design notes in `docs/event-subprocess-contract.md`.
This release seals the `v1.0.52` line with personal event subscriptions, a deterministic 22-product Agent command catalog, local user-operation auditing, expanded Open product commands, safer macOS credentials and release signing, and more reliable Connect and IM delivery.
### Added
- **`dws schema` for registered local commands** — `dws schema "event consume"` (or `event.consume`) now returns a machine-readable input schema synthesized from the command's cobra flags, in the same flat shape helper subtrees emit: `{description, path, source, parameters{<flag>:{type, required, description, default?}}}` plus an `arguments` array for positional inputs, with `source: "cobra"` distinguishing flag-synthesized schema from MCP-fetched (`mcp:<server>`). Intermediate nodes (`dws schema event`) list their subcommands. The mechanism is a reusable registry (`cobraSchemaRoots`); `event` is the first consumer and more command trees can opt in without further wiring. Inherited global flags and hidden internal flags are excluded so the schema describes just that command.
- **Safe macOS Keychain → file-DEK migration** — `dws auth migrate-keychain --to file-dek` preflights every legacy/profile auth entry before rewriting, ignores unrelated application secrets, supports side-effect-free `--dry-run`, requires explicit `--yes`, and lets sandboxed and normal processes share an existing login without exposing tokens.
- **Personal event subscriptions** (#589) — adds `dws event list/schema/consume/status/stop` for user @ mentions, selected one-to-one chats, and selected group chats. `consume` can create or reuse a personal subscription, multiple local consumers share one bus while keeping outputs isolated by event type and subscription, and the mono/multi event Skills ship with the binary.
- **Open product command capabilities** (#608) — adds Sheet table, pivot-table, and gridline commands; Chat message favorites; Drive statistics and shortcuts; and Doc comment update/delete, with matching mono/multi Skill documentation and command-contract coverage.
- **Local user-operation audit log** (#555) — operations executed through `dws` now produce redacted daily JSONL records with actor, command and endpoint, result or error category, duration, CLI/platform metadata, and a SHA-256 previous-hash chain for tamper evidence. Writers coordinate through a cross-process file lock and rotate logs safely; `dws audit tail` inspects recent records, `dws audit export` emits date-filtered JSONL or CSV, and `dws audit verify` reports the first broken link in a file's hash chain.
- **Stable Agent command catalog** (#598) — `dws schema` now ships a deterministic 22-product / 564-tool catalog generated from the executable Cobra tree, with progressive product/group/leaf queries, complete parameter contracts, reviewed command identity and aliases, safety/confirmation metadata, field provenance, and final-delivery completeness/drift gates. The catalog is embedded at build time and does not require runtime MCP `tools/list` discovery.
- **Reviewed Schema for local commands** (#598, #609) — `event consume/list/schema/status/stop` and `audit export/tail/verify` enter the reviewed `CommandRegistry`, bind to the real Cobra tree at generation time, and ship through the same typed `ToolSpec` and embedded Catalog path as public MCP-backed commands. Leaf, group, product, and `--all` queries are projections of that single delivered model.
- **Safe macOS Keychain → file-DEK migration** (#597) — `dws auth migrate-keychain --to file-dek` preflights every legacy/profile auth entry before rewriting, ignores unrelated application secrets, supports side-effect-free `--dry-run`, requires explicit `--yes`, and lets sandboxed and normal processes share an existing login without exposing tokens.
### Changed
- **`event consume` AI-subprocess contract** (#609) — emits a fixed ready line and a final controlled-exit summary, supports parent-pipe stdin EOF as graceful shutdown, forwards `--profile` to the detached bus, surfaces bus startup errors, and cleans up subscriptions according to ownership so orchestrators can drive event streams without sleeps or leaked server-side subscriptions.
- **Wukong IM read-result parity** (#618) — `chat message list` preserves quoted merged-forward and image context; message-search entitlement failures retain the server-provided friendly hint and action URL; and `ding message list` exposes each DING's content alongside its ID and status.
- **Developer ID signing for official macOS archives** (#605) — official releases now require both Darwin archives to be signed with the configured Apple Developer ID certificate, timestamp, and hardened runtime. The release job validates credentials and signatures and fails closed instead of silently publishing ad-hoc-signed official binaries.
### Fixed
- **Cross-platform auth regression coverage** — dedicated macOS CI now runs the Darwin-only auth/keychain regression suite with race detection, Windows CI builds and tests the native DPAPI path, and recovery guidance prefers safe migration or per-profile cleanup over destructive global reset.
- **Smart-category mappings and runtime network diagnostics** (#591) — `chat category create-smart` now maps category names, group-name keywords, and member OpenDingTalk IDs to the live MCP contract, rejects blank or empty supplied values locally, and reports runtime `tools/call` connection failures as actionable API/network errors instead of internal discovery failures.
- **Connect daemon restart lifecycle** (#599) — pins the Stream SDK reconnect-race fix, snapshots the running executable before detaching, uses a real 30-second keepalive, and manages each worker as its own Unix process group so launcher cleanup or worker panics no longer cause restart loops or orphan local-agent processes.
- **Complex Connect messages and attachments** (#606, #612) — rich-text messages retain all embedded pictures in order, queued turns keep every pending attachment, and unknown or future callback shapes reach each Agent backend with their message type and raw JSON instead of being discarded. Attachment recovery is locator-based, nested `chatRecord` pictures/audio/video/files can be recovered from message APIs after Stream ACK, and OpenCode uses a full-duration storyboard for large videos to avoid base64 OOMs while preserving the original download for the turn.
- **macOS auth survives Keychain mode changes** (#597) — credential reads try existing compatible DEKs without creating key material, updates preserve the DEK that decrypted existing ciphertext, unreadable slots fail closed before token exchange, profile slots use the canonical auth backend, and `auth status` reports ciphertext/key mismatches instead of treating them as ordinary logout. Dedicated macOS race and Windows DPAPI coverage protect the cross-platform paths.
## [1.0.51] - 2026-07-10
+49 -2
View File
@@ -1,6 +1,6 @@
GO ?= go
.PHONY: all help build rebuild test lint fmt policy edition-test package release publish-homebrew-formula setup-hooks
.PHONY: all help build rebuild test lint fmt policy edition-test test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release publish-homebrew-formula setup-hooks
all: setup-hooks fmt lint build test rebuild
@@ -10,7 +10,11 @@ help:
@printf " make test - Run the Go test suite\n"
@printf " make lint - Run formatting checks and golangci-lint when available\n"
@printf " make fmt - Format Go source files\n"
@printf " make policy - Run open-source asset and command-surface checks\n"
@printf " make policy - Run open-source asset and Schema registry checks\n"
@printf " make test-schema-agent-examples - Contract-check all Agent examples and dry-run the eligible subset\n"
@printf " make generate-schema - Regenerate embedded Agent metadata and the release Catalog\n"
@printf " make generate-schema-agent-metadata - Regenerate versioned Agent metadata\n"
@printf " make generate-schema-catalog - Regenerate the embedded release Catalog\n"
@printf " make package - Build all release artifacts locally (goreleaser snapshot)\n"
@printf " make release - Build and publish a release via goreleaser\n"
@printf " make publish-homebrew-formula - Push dist/homebrew/dingtalk-workspace-cli.rb to a tap repo\n"
@@ -32,11 +36,54 @@ fmt:
policy:
@./scripts/policy/check-open-source-assets.sh
@./scripts/policy/check-schema-command-registry.sh
@./scripts/policy/check-command-surface.sh --strict
@./scripts/policy/check-generated-drift.sh
@./scripts/policy/check-schema-catalog.sh
@./scripts/policy/check-schema-binary.sh
@$(MAKE) test-schema-agent-examples
edition-test:
$(GO) test -v -count=1 ./pkg/editiontest/...
test-schema-agent-examples:
DWS_AGENT_EXAMPLES_DRY_RUN=1 $(GO) test -v -count=1 ./internal/app -run '^TestManualAgentExamplesDryRun$$'
generate-schema:
@set -e; \
registry_guard=$$(mktemp); \
metadata_guard=$$(mktemp -d); \
selection_guard=$$(mktemp -d); \
trap 'rm -rf "$$registry_guard" "$$metadata_guard" "$$selection_guard"' EXIT HUP INT TERM; \
cp internal/cli/schema_command_registry.json "$$registry_guard"; \
cp -R internal/cli/schema_hints/metadata/. "$$metadata_guard/"; \
cp -R internal/cli/schema_hints/selection/. "$$selection_guard/"; \
$(GO) generate ./internal/cli; \
cmp -s internal/cli/schema_command_registry.json "$$registry_guard" || { \
printf '%s\n' 'generation modified reviewed input internal/cli/schema_command_registry.json' >&2; \
exit 1; \
}; \
diff -qr internal/cli/schema_hints/metadata "$$metadata_guard" >/dev/null || { \
printf '%s\n' 'generation modified reviewed input internal/cli/schema_hints/metadata' >&2; \
exit 1; \
}; \
diff -qr internal/cli/schema_hints/selection "$$selection_guard" >/dev/null || { \
printf '%s\n' 'generation modified reviewed input internal/cli/schema_hints/selection' >&2; \
exit 1; \
}
generate-schema-agent-metadata:
$(GO) run ./internal/generator/cmd_schema_agent_metadata \
-root . \
-registry internal/cli/schema_command_registry.json \
-output-dir internal/cli/schema_agent_metadata \
-audit-output internal/cli/schema_agent_metadata_audit.json
generate-schema-catalog:
$(GO) run -a ./internal/generator/cmd_schema_catalog \
-root . \
-output internal/cli/schema_catalog.json
package:
@./scripts/dev/build-all.sh
@./scripts/release/post-goreleaser.sh
+24 -13
View File
@@ -71,9 +71,9 @@ The installer ships skills in one of two layouts. CLI commands (`dws aitable ...
| Mode | What gets installed | Best for |
|------|----------------------|----------|
| **mono** (stable, default) | One `dws` skill covering all products | Cross-product workflows; single entry point |
| **multi** 🧪 **EXPERIMENTAL** | 22 per-product skills (`dingtalk-aitable`, `dingtalk-calendar`, `dingtalk-chat`, ...) | Single-product tasks; smaller context per call |
| **multi** 🧪 **EXPERIMENTAL** | Per-product skills (`dingtalk-aitable`, `dingtalk-calendar`, `dingtalk-chat`, ...) | Single-product tasks; smaller context per call |
> 🧪 **`multi` is currently EXPERIMENTAL / preview.** 22 product-scoped skills all pass the dispatch verifier, but interface, naming and cross-skill references may change in future releases. For production / shared environments, prefer `mono`. File issues if you hit problems.
> 🧪 **`multi` is currently EXPERIMENTAL / preview.** All product-scoped skills pass the dispatch verifier, but interface, naming and cross-skill references may change in future releases. For production / shared environments, prefer `mono`. File issues if you hit problems.
How to pick:
@@ -323,25 +323,35 @@ dws contact user get-self --jq '.result[0].orgEmployeeModel | {name: .orgUserNam
### Command Help and Schema
Product commands are compiled into the binary in static endpoint mode. Use `--help` and the bundled Agent Skills as the source of truth; `dws schema` is retained for helper-only schemas such as `dev.*`.
Use Cobra help and Schema for different parts of the command contract:
- `dws <path> --help` is the source of truth for whether a command exists and which flags the binary accepts.
- `dws schema "<path>"` is the Agent contract for command selection, parameter mappings and constraints, risk, and confirmation semantics.
- If Help and Schema disagree, treat it as contract drift: pass only flags accepted by Cobra and use the more conservative safety semantics.
- Schema describes commands; it does not read or search DingTalk business data. Execute the real product command after discovery.
```bash
# Inspect the current compiled command surface
# Confirm that the command exists and inspect accepted flags
dws aitable record query --help
# Helper-only schema introspection
dws schema "dev app create"
# Discover within a product, then inspect the selected leaf contract
dws schema aitable
dws schema "aitable record query"
# Construct the call
# Execute the real business query
dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
```
`dws schema --all` exports the complete contract for tooling, CI, audits, and compatibility baselines. Agents should prefer product/group discovery followed by a leaf query to avoid loading the full Catalog into context.
### Agent Skills
The repo ships a complete Agent Skill system under `skills/`, now organized into two layouts:
The repo ships a complete Agent Skill system under `skills/`, organized into two layouts:
- `skills/mono/` — single-skill layout (one `SKILL.md` + `references/products/`), recommended default.
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ... 22 products in total), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.**
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ...), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.**
Shared reviewed inputs for Schema generation live separately under `internal/cli/schema_hints/`. They are not Agent Skills and are excluded from binaries and release skill bundles.
After installing, AI tools like Claude Code / Cursor can operate DingTalk directly through natural language:
@@ -551,12 +561,13 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --fields invocati
</details>
<details>
<summary><strong>Schema Introspection</strong> — helper-only schemas in static endpoint mode</summary>
<summary><strong>Schema Introspection</strong> — Agent command discovery and execution contracts</summary>
```bash
dws schema # static endpoint mode note
dws schema "dev app create" # view helper-only schema
dws schema "dev app create" --jq '.tool.required' # view required fields
dws schema aitable # discover product commands
dws schema "aitable record query" # view the selected leaf contract
dws schema "aitable record query" --jq '.tool.required' # view required fields
dws schema --all # full export for CI/audit/baselines
```
</details>
+24 -13
View File
@@ -71,9 +71,9 @@ irm https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/ma
| 模式 | 安装内容 | 适合场景 |
|------|----------|----------|
| **mono**(稳定,默认) | 一个 `dws` skill,覆盖全部产品 | 跨产品组合操作;单一入口召唤 |
| **multi** 🧪 **试验版 / Preview** | 22 个独立产品 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 |
| **multi** 🧪 **试验版 / Preview** | 按产品拆分的独立 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 |
> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。22 个独立 skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。
> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。全部独立 skill 均通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。
怎么选:
@@ -320,25 +320,35 @@ dws contact user get-self --jq '.result[0].orgEmployeeModel | {name: .orgUserNam
### 命令帮助与 Schema
产品命令在静态端点模式下已经编译进二进制。Agent 以 `--help` 和内置 Skill 为事实源;`dws schema` 仅保留给 `dev.*` 等 helper-only schema 查询。
命令帮助和 Schema 分别负责命令契约的不同部分:
- `dws <path> --help` 是命令是否存在、当前二进制接受哪些 flags 的事实源。
- `dws schema "<path>"` 是 Agent 选命令、参数映射与约束、风险和确认语义的契约。
- Help 与 Schema 冲突时视为契约漂移:执行只传 Cobra 接受的参数,安全语义取更保守值。
- Schema 只描述命令,不读取或搜索钉钉业务数据;发现命令后仍需执行真实产品命令。
```bash
# 查看当前编译出的命令面
# 确认命令存在并查看当前接受的 flags
dws aitable record query --help
# helper-only schema 自省
dws schema "dev app create"
# 先在产品内发现命令,再查看选中 leaf 的契约
dws schema aitable
dws schema "aitable record query"
# 构造正确的调用
# 执行真实业务查询
dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
```
`dws schema --all` 会完整导出命令契约,供工具、CI、审计和兼容性基线使用。Agent 应优先按产品/分组发现后查询 leaf,避免把整个 Catalog 加载进上下文。
### Agent Skills
仓库内置完整的 Agent Skill 体系(`skills/` 目录),目前重组为两套布局:
仓库内置完整的 Agent Skill 体系(`skills/` 目录),分为两套布局:
- `skills/mono/` — 单 skill 布局(一个 `SKILL.md` + `references/products/`),默认推荐。
- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ... 共 22 个),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。**
- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ...),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。**
Schema 生成共享的 reviewed 输入单独位于 `internal/cli/schema_hints/`。它们不是 Agent Skill,也不会进入二进制或发布 skill 包。
安装之后,Claude Code / Cursor 等 AI 工具就能通过自然语言直接操作钉钉:
@@ -548,12 +558,13 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --fields invocati
</details>
<details>
<summary><strong>Schema 自省</strong> — 静态端点模式下的 helper-only schema</summary>
<summary><strong>Schema 自省</strong> — Agent 命令发现与执行契约</summary>
```bash
dws schema # 静态端点模式提示
dws schema "dev app create" # 查看 helper-only schema
dws schema "dev app create" --jq '.tool.required' # 查看必填字段
dws schema aitable # 发现产品命令
dws schema "aitable record query" # 查看选中 leaf 契约
dws schema "aitable record query" --jq '.tool.required' # 查看必填字段
dws schema --all # CI/审计/基线的全量导出
```
</details>
+8 -4
View File
@@ -1,22 +1,26 @@
# Architecture
`dws` is a Go CLI that turns DingTalk MCP metadata into a command-line surface for both humans and AI agents.
`dws` is a Go CLI with a versioned, static command surface for DingTalk MCP capabilities. Cobra help serves humans; the embedded Command Catalog serves AI agents.
## High-Level Flow
1. `cmd` is the CLI entrypoint, invoking `internal/app` to build the root Cobra command tree.
2. `internal/app` wires static utility commands (`auth`, `audit`, `schema`, `completion`), product helper commands, and plugin commands.
2. `internal/app` wires static utility commands (`auth`, `audit`, `schema`, `completion`), product helpers, and versioned plugin descriptors.
3. `internal/helpers` contains the main command handlers for all product surfaces (`dev`, `chat`, `calendar`, `contact`, `aitable`, etc.).
4. `internal/executor` and `internal/transport` execute MCP JSON-RPC calls; `internal/output` formats responses.
5. `internal/auth` manages login state, PAT tokens, and agent-code detection.
6. Schema generation starts from the reviewed `CommandRegistry`, binds each identity to the exact current Cobra leaf, and then resolves typed constraints, sanitized MCP snapshots, Agent hints, and Skills into one `SchemaRegistry`. Startup and Schema queries do not call MCP `tools/list`.
7. The embedded Catalog is a downstream release artifact and never backfills identity or participates in regeneration. Stable flag-to-interface property bindings come from the reviewed, content-addressed v3 manifest in `schema_parameter_bindings.json`; its exact active tuples, corrections, removals, and mapping exclusions are validated against the final bound `SchemaRegistry`. CLI `required` and constraints come from the resolved typed contract, while MCP `required` remains interface-only metadata.
8. Agent selection results are fixed in versioned review inputs. Every public tool has explicit use/avoid/example and interface disposition metadata; Skill references that are not current leaves require an explicit alias/group/stale/out-of-surface review instead of fuzzy runtime matching.
## Repository Structure
- `cmd`: CLI entrypoint
- `internal/app`: root command wiring, static utility commands, and plugin loading
- `internal/helpers`: product command handlers (dev, chat, calendar, contact, etc.)
- `internal/plugin`: plugin-based dynamic command loader
- `internal/cli`: catalog types and endpoint loader (static endpoint mode)
- `internal/plugin`: versioned plugin manifest, hook, skill, and transport descriptor loading
- `internal/cli`: embedded Agent Command Catalog, static schema query, and catalog contracts
- `internal/generator`: deterministic Agent metadata and Command Catalog generators
- `internal/executor`: invocation dispatch and result handling
- `internal/transport`: MCP HTTP client and request signing
- `internal/auth`: login, token management, agent-code detection, identity
+2 -2
View File
@@ -147,8 +147,8 @@ _Group chats, conversations, messages, and robot/webhook integrations._
| `dws chat group members remove` | Remove one or more members from a group chat. | When the agent kicks users who should no longer have access to the group. |
| `dws chat group rename` | Update the display name of a group chat. | When the agent is rebranding or clarifying the purpose of an existing group. |
| `dws chat list-top-conversations` | Fetch the list of conversations the current user has pinned to the top of their chat list. | When the agent needs to prioritize the user's most important conversations in a summary or dashboard. |
| `dws chat message list` | Pull the recent message history of a specific conversation (v2), paginated. | When the agent needs to read what has recently been said in a conversation to summarize or reason about it. |
| `dws chat message list-all` | Search all messages across the current user's conversations within a time range. | When the agent needs to audit or summarize everything the user saw across chats in a window. |
| `dws chat message list` | Pull the recent message history of a specific conversation, including quoted-message context for merged forwards and images. | When the agent needs to read what has recently been said in a conversation and retain the context of replies. |
| `dws chat message list-all` | Search all messages across the current user's conversations within a time range, surfacing any search-entitlement guidance. | When the agent needs to audit or summarize everything the user saw across chats in a window. |
| `dws chat message list-by-sender` | Fetch messages authored by a specific sender across both single and group chats. | When the agent needs to pull everything a particular colleague said recently. |
| `dws chat message list-focused` | Fetch messages from users the current user has marked as "special focus" (starred contacts). | When the agent builds a priority-inbox view highlighting messages from important people. |
| `dws chat message list-mentions` | Fetch messages where the current user was @-mentioned. | When the agent wants to surface items that explicitly require the user's attention. |
+38 -19
View File
@@ -34,7 +34,7 @@ With `-f json`, error responses include structured payloads: `category`, `reason
dws contact user search --query "Alice" -f table # Table (default, human-friendly / 表格,默认)
dws contact user search --query "Alice" -f json # JSON (for agents and piping / 适合 agent)
dws contact user search --query "Alice" -f raw # Raw API response / 原始响应
dws schema -f pretty "dev app create" # Pretty helper-only schema view / helper-only schema 彩色分区展示
dws schema -f pretty "calendar event create" # Pretty Agent schema view / Agent Schema 彩色查看
```
## Dry Run / 试运行
@@ -51,40 +51,59 @@ dws contact user search --query "Alice" -o result.json
## Schema Introspection / Schema 查询
静态端点模式下,产品命令和 flag 以当前二进制的 `--help` 与内置 Skill 为准。`dws schema` 仅保留 helper-only 子树(如 `dev.*`)的 schema 查询。
`--help` 展示当前二进制的 Cobra 命令和可接受 flag,`dws schema` 查询同版本内嵌的 Agent 命令契约。Schema 查询不访问 MCP endpoint、不执行 `tools/list`,也不搜索钉钉文档或任何业务数据。
Schema 的稳定 `canonical_path`、主 CLI 路径和 aliases 来自 reviewed `CommandRegistry`,并在发布时逐项绑定当前 Cobra tree。编辑 `internal/cli/schema_command_registry.json` 时必须遵守同目录的 `schema_command_registry.schema.json`;普通生成流程只校验该 reviewed input,不会覆盖它。Native annotation 只做实现一致性校验;Catalog 是该统一强类型契约的发布输出,不作为命令发现或下一轮生成的输入。
### 路径写法
```bash
dws schema # 静态端点模式提示
dws schema "dev app create" # CLI 空格路径
dws schema --cli-path "dev app create" # 显式 flag(脚本友好,免转义)
dws schema -f pretty "dev app create" # ANSI 着色分区展示(人肉查看最舒服)
dws schema # 当前公开产品面的紧凑概览
dws schema calendar # 展开一个产品
dws schema "calendar event" # 展开一个命令分组
dws schema "calendar event create" # 按 CLI 空格路径查询工具
dws schema calendar.create_calendar_event # 按 canonical path 查询工具
dws schema --cli-path "calendar event create" # 显式 CLI path
dws schema "calendar event create" --compact # 支持:省略 provenance/debug 字段
dws schema --all # 全部工具的完整 leaf Schema,用于审计/CI/baseline
```
helper-only schema 以 CLI 路径为准;普通产品命令请使用 `dws <path> --help` 查看参数。
兼容入口 `dws schema list` 等价于根概览。`schema --all` 是完整导出:每个工具都包含完整 leaf 参数、约束和安全语义。它输出很大,只用于明确要求的全量导出、审计、CI 或参数 baseline;普通 Agent 任务应按概览、产品/分组、leaf 渐进查询,不要把 `--all` 直接注入上下文。`schema --all --compact` 虽受支持,但会裁掉 provenance 和接口映射字段,不能作为完整 baseline。
Leaf 查询、`--all` 中对应工具和 Catalog full tool 均由同一个 resolved `ToolSpec` 投影,内容必须一致;概览、产品/分组和 Catalog summary 也由该 `ToolSpec` 的统一 summary 投影生成。通过 alias 查询时,只允许 `cli_path` 和 `is_alias` 发生视图变化,参数、安全和接口契约不得变化。
`--compact` 是 Schema 的展示选项。当前版本支持该 flag;若兼容旧二进制时收到 `unknown_flag: --compact`,用同一个 Schema 查询去掉 `--compact` 重试。这只降低输出裁剪能力,不表示 leaf 不存在,也不能改用 Schema 查询业务数据。
### Schema、Help 与业务数据的边界
| 问题 | 事实源 |
|------|--------|
| 命令是否由当前二进制暴露、Cobra 接受哪些 flags | `dws <path> --help` |
| Agent 选哪个命令、参数映射与组合约束、risk/confirmation | 对应的 leaf `dws schema "<path>"` |
| 当前钉钉中的文档、文件、日程、消息等业务数据 | 实际执行 `dws doc read`、`dws drive search` 等 read/search/list 命令 |
Schema 与 Help 冲突表示发布契约漂移,不能静默猜测。执行参数必须以 Cobra 实际接受的 flag 为准;安全语义冲突时采用更保守的处理(例如先确认)或停止执行并报告漂移。完成命令发现后,仍必须执行真实业务命令;`dws schema` 本身不会读取或搜索业务内容。
### 单工具输出字段
| 字段 | 说明 |
|------|------|
| `name` / `cli_name` / `canonical_path` | MCP RPC 名 / CLI 叶子名 / helper-only canonical path |
| `group` | CLI 父级 group 路径(dot-separated) |
| `title` / `description` | 工具名/说明(overlay 优先) |
| `parameters` / `required` | MCP 输入 JSON Schema 的 properties / required |
| `output_schema` | MCP 输出 Schema(上游下发时才有) |
| `sensitive` | 敏感写操作,需 `--yes` 确认 |
| `auth` | DingTalk 授权元数据,包括 `requiredScopes` / `requiredPermissions` / `recommendedScopes` / `grantProductCodes` / `riskAction` / `confirmationRequired` |
| `annotations.destructive_hint` | 对齐 MCP 2025+ annotations,目前从 `sensitive` 映射 |
| `flag_overlay[param]` | CLI 层对 MCP 参数的改写:`alias` / `transform` / `transform_args` / `env_default` / `default` / `hidden` |
| `canonical_path` / `primary_cli_path` / `aliases` | 稳定工具 ID、主 CLI 路径和兼容路径 |
| `product_id` / `interface_ref` | CLI 产品与实际 MCP product/RPC binding |
| `title` / `description` / `agent_summary` | 人类说明、接口说明和 Agent 摘要 |
| `parameters.<flag>` | CLI flag 的类型、属性名、required、默认值、格式、枚举和条件必填 |
| `constraints` | one-of、互斥、联动等组合约束 |
| `effect` / `risk` / `confirmation` / `idempotency` | Agent 执行与安全策略 |
| `use_when` / `avoid_when` / `examples` | Agent 选择提示和示例 |
| `reviewed` / `agent_source_refs` | 语义审核状态与来源追踪 |
**调试 `--flag` 行为的第一站**是 `flag_overlay` —— 比如 `--users 0232...` 能不能直接用,看 `receiverUserIdList.transform == "csv_to_array"` 即可判断。
`parameters.<flag>.required` 是按来源 precedence 解析后的 Agent 参数契约;`cli_required=true` 才表示 Cobra 将该 flag 标记为硬必填。条件必填或别名选择通过 `required_when` 和 `constraints.require_one_of` 表达。`required` 不直接复制 MCP input schema,也不取代 Cobra 的实际执行校验。
### 筛选输出
```bash
dws schema "dev app create" --jq '.tool.parameters' # 只看参数 schema
dws schema "dev app create" --jq '.tool.required' # 只看必填字段
dws schema "calendar event create" --jq '.parameters' # 只看参数
dws schema "calendar event create" --jq '[.parameters | to_entries[] | select(.value.required)]' # 只看 Agent required 参数
```
## Shell Completion / 自动补全
+311
View File
@@ -0,0 +1,311 @@
# DWS Agent Schema 统一方案
## 1. 核心定义
DWS Schema 是当前二进制公开 CLI 的版本化 Agent 执行契约。它描述真实 Cobra 命令,并补充 Agent 选择、参数映射、组合约束、安全确认和接口事实。
设计遵循三条硬规则:
1. **Schema 描述 CLI,不制造 CLI。** `CommandRegistry`、manual hint、metadata 和 Catalog 都不能凭空创建 Cobra 命令或 flag;registry 中的每个路径都必须精确绑定真实 runnable Cobra leaf。
2. **所有来源只解析一次。** 来源经过统一 resolver 进入 typed `SchemaRegistry`,所有查询、导出和门禁都消费同一个 `SchemaRegistry/SchemaIndex`。
3. **Registry-first,Catalog 只出不进。** reviewed `CommandRegistry` 是稳定 command identity/navigation 的唯一事实源;`schema_catalog.json` 和其他生成 JSON 只是下游发布物,不能成为命令、metadata 或下一轮 Catalog 的来源。运行时 production loader 解码 embedded snapshot 只是交付边界,不是 source resolution。
Schema 不调用 MCP `tools/list`,不访问网络,也不读取用户本地 discovery cache。
## 2. 单向数据流
```text
schema_command_registry.json (reviewed CommandRegistry source)
+ reviewed manual command additions
|
v
EffectiveCommandRegistry
|
v
exact binder to live Cobra tree
+ native identity consistency assertions
|
v
BoundCommandRegistry
|
+----------------------+
|
skills/mono Markdown + internal/cli/schema_hints/*.json |
+ schema_mcp_metadata.json |
| |
v |
Agent-metadata normalization |
| |
v |
schema_agent_metadata/*.json |
(generated normalized input) |
| |
+-----------------------+
|
live Cobra flag facts / typed parameter metadata
+ schema_hints/metadata/*.json (reviewed parameter overlay + safety)
+ schema_hints/selection/*.json (reviewed Agent selection prose)
+ schema_parameter_bindings.json (reviewed flag -> RPC property)
+ schema_mcp_metadata.json (pinned, sanitized interface facts)
+ normalized Agent metadata
|
v
source adapters + resolvers
|
v
one typed SchemaRegistry
(one ToolSpec per command)
+
typed SchemaIndex
+-----------+-----------+
| |
v v
build-time typed gates snapshot serializer
|
v
schema_catalog.json
(release output only)
|
v
go:embed -> typed loader
|
v
SchemaRegistry + SchemaIndex
|
+---------------------+------------------+
| | |
overview/product/group leaf --all
projections projection full projection
| | |
+---------------------+------------------+
|
v
runtime query + delivery gates
```
`--help` 是 Cobra 自身的人类可读投影,不从 Catalog 生成。Schema projections 和 `--help` 共享同一真实 Cobra 命令面,但承担不同职责。Binder 之后不得再从 annotation、manual hint 或生成 JSON 重新解析 command identity。
## 3. 与 Lark 的关系
DWS 与 Lark 保持**架构同构**,而不是强行复制字段:
| Lark 分层 | DWS 对应层 |
|---|---|
| typed command/metadata registry | `EffectiveCommandRegistry`、`BoundCommandRegistry` 与最终 `SchemaRegistry` |
| navigation catalog/index | 从同一 `ToolSpec` 派生的 `SchemaIndex` |
| schema renderer/envelope | overview、product/group、leaf、`--all` projections |
共同点是:强类型 registry 持有已审核、已绑定、已解析的事实,index 只负责确定性导航,renderer 只投影,不重新读取来源或做 precedence。DWS 的 base Registry 与 reviewed manual command additions 在绑定前合并为唯一的 `EffectiveCommandRegistry`,因此不存在 “native-first”、“legacy registry fallback” 或 Catalog fallback。
DWS 内部 resolved model 为:
```text
SchemaRegistry
-> []ProductSpec
-> []ToolSpec
-> ToolIdentitySpec
-> []ParameterSpec
-> RuntimeSchemaConstraints + []RuntimeSchemaPositional
-> SafetySpec
-> InterfaceSpec
-> SelectionSpec
-> map[field]FieldProvenance
```
字段合并和 precedence 在进入该模型前完成。`map[string]any`/flat JSON 只允许存在于 renderer 和 snapshot/wire boundary,不能作为内部 resolver、navigation 或 gate 的第二套数据模型。
DWS 当前对外仍保留兼容 wire:leaf 使用 flat `parameters`,安全和选择字段也保持现有键名。架构对齐不等于未版本化地切换到 Lark `inputSchema/outputSchema/_meta` envelope;若未来提供该格式,应作为明确版本的新投影,并保留现有兼容输出。
## 4. 来源职责
| 来源 | 负责内容 | 明确不负责 |
|---|---|---|
| `schema_command_registry.json` | reviewed `CommandRegistry`:稳定 canonical identity、primary CLI path、alias、exposure 和导航 | 创建 Cobra 命令/flag、参数、安全、endpoint/token |
| reviewed manual command additions | 将一个精确存在的 runnable Cobra leaf 合并进 `EffectiveCommandRegistry`;必须 reviewed 且带 reason | 运行时 fallback、覆盖冲突 identity、创建命令 |
| Go/Cobra | 路径是否真实可执行、Cobra 接受的 flag、CLI 类型/默认值、执行校验、help 文本 | 稳定 canonical identity、Agent 场景选择、虚构 RPC |
| native Schema identity annotations | implementation-side consistency evidence;存在时必须与 `EffectiveCommandRegistry` 精确一致 | 提供、补全、推断或覆盖 identity |
| `schema_hints/metadata/*.json` parameter overlays | 精确覆盖现有 flag 的描述、映射、类型和 required 语义;并承载 safety / `runtime_gate` / interface | 创建命令/flag、绕过 completeness、虚构 RPC |
| typed parameter metadata / constraints | `required_when`、one-of、互斥、联动、格式、枚举、位置参数 | 命令 identity |
| `schema_parameter_bindings.json` | 稳定 CLI flag 到 RPC property 的映射 | 命令发现、risk 推断 |
| `schema_mcp_metadata.json` | pinned RPC identity、接口描述和脱敏参数事实 | CLI identity、运行时路由、risk 推断 |
| `schema_hints/selection/*.json` | reviewed selection prose(summary / use_when / avoid_when / examples) | 创建 Cobra 命令或参数、改写 safety |
| Skills/Markdown | 产品路由、工作流和使用建议 | 命令存在性和 flag 事实 |
| `schema_catalog.json` 及其他 generated JSON | resolved registry 的兼容发布序列化;运行时由 production loader 解回 typed registry/index | generation/source resolution 输入、identity fallback、手工修复源 |
`schema_command_registry.json` 承载 reviewed `CommandRegistry`。Manual command addition 先以确定性规则合并进 effective registry;从 binder 开始,下游只看到一个稳定 identity/navigation 模型。旧 wire 中的 `surface_hash` / `surface_tools` 字段仅为兼容名称,语义已经是 effective Registry hash/coverage,不构成第二事实源。
## 5. 统一解析与 precedence
### 5.1 Identity
- Reviewed base `CommandRegistry` 是 stable canonical identity、primary path、alias 和 navigation 的唯一基础事实源。
- Reviewed manual command addition 只能引用精确存在的 runnable Cobra leaf;它在绑定前合并进 `EffectiveCommandRegistry`。若与 base Registry 的 identity/path/alias 冲突,生成失败,不能按 precedence 静默覆盖。
- Binder 必须把 effective entry 的 primary path 和每个 alias 精确解析到同一个真实 executable leaf;stale path、phantom path、重复 identity 或 alias collision 全部失败。
- Native identity annotation 是可选的一致性证据:存在时必须与 effective entry 精确一致;缺失不触发补写、推断或 fallback。
- Public runnable Cobra leaf 未进入 effective registry 时,必须存在 exact、reviewed、带 reason 的 exclusion;不得用 prefix/wildcard 排除。
- Identity 不做名称推断,不从 Catalog/generated metadata fallback,也没有多来源 winner。
删除 native materialization 前已做写入审计:旧
`ApplyNativeRuntimeSchemaContracts` 的唯一写操作是对已存在命令调用
`AttachRuntimeSchema`,只写 command identity 的 product/tool/source annotation;
它不写 flag property/type/required、constraints、positionals、title/description
或 interface mapping。这些字段原本已分别由 parameter binding/metadata、
constraint、Cobra help 和 interface resolver 提供,因此删除该过渡层没有数据迁移缺口。
CI 同时禁止重新加入 generated native contracts 或 materialization 入口。
#### CommandRegistry 输入审计
`schema_command_registry.json` 是 reviewed source,不是生成快照。它必须保留
`$schema: ./schema_command_registry.schema.json`。该 JSON Schema 对 root、product
和 CommandSpec 全部使用 `additionalProperties: false`,并约束:
- canonical identity、`source_product_id` 和精确 CLI path 的格式;
- `aliases` 唯一且不能复用 primary path;
- `visibility` 只允许 `public | compat | internal`,省略时明确归一化为
`public`;
- primary path、alias、canonical 和 product 之间无法由 JSON Schema 表达的
交叉约束,继续由 Go strict loader 和 Cobra binder fail-closed 校验。
Registry semantic hash 覆盖 canonical、primary CLI path、alias 集合、
`source_product_id` 和 normalized visibility。格式、顺序以及省略的等价默认值
不改变 hash;上述任一稳定契约字段变化都必须改变 hash。测试逐字段验证这一点,
不使用当前命令数量作为常量。
普通 `go generate ./internal/cli` 只把 Registry 作为 validation-only 输入并生成
Agent metadata/Catalog 等单向下游资产,不生成或覆盖 Registry。drift policy 在生成
前后对 reviewed Registry 做 byte-for-byte guard;独立的
`check-schema-command-registry.sh` 在 interface/provenance/Catalog policy 之前检查
JSON 输入契约、禁用旧 native materialization 符号,并从 Registry 动态计算审计
数量,不能硬编码某次快照的 tool count。
### 5.2 Parameter
每个字段按明确的来源 precedence 选择一次,并把 winner、候选值和来源写入 provenance。precedence **与值无关**:不能因为 `required=true` 看起来更严格就让它越级获胜。更高优先级的 reviewed manual override 可以把 `required`、映射、interface type 或描述调高,也可以调低。
实现中的参数字段顺序固定为:
```text
reviewed manual > versioned binding > command constraint > typed metadata
> native/Cobra contract > ToolSchemaHint > MCP metadata
> inference/default
```
命令 `title` / `description` 使用独立但同样确定的文本顺序:
```text
reviewed ToolSchemaHint > command-specific Cobra Help > MCP metadata > inference
```
因此多个 CLI leaf 复用同一个 RPC 时,通用 RPC 文案只能作为未选中的
provenance candidate 保留;参数级 RPC 文案可进入 `interface_description`,
但不得覆盖 leaf 自己的标题和执行语义。
Cobra hard-required 是独立的 executable fact,并通过 `cli_required`/provenance 保留;它不应在 renderer 中再次静默改写已经解析的 Agent projection。
### 5.3 Safety、selection 与 interface
`effect`、`risk`、`confirmation`、`idempotency`、selection 和 interface disposition 同样按 source precedence 解析,而不是按值的“严格程度”合并。更高优先级的 reviewed explicit/manual source 可以升高或降低最终值;同 precedence 的不同值必须报冲突。
最终 interface disposition 还必须满足 conflict matrix:
- `mode` 与 `availability` 正交:`mode` 只允许 `mcp | local | composite`,`availability` 只允许 `available | unavailable`;`unavailable` 不是第四种 mode。
- `mcp + available`:只表示命令可由一个 pinned、参数可映射且语义等价的 `interface_ref` 完整表达;本地 wrapper 只是固定默认值或投影返回值时,也必须先证明参数和执行语义没有漂移。
- `local + available`:仅用于纯本地进程、静态数据或策略操作,不得携带 direct `interface_ref`;“远端 RPC 尚未进入 pinned metadata”不能归类为 local。
- `composite + available`:用于多 RPC、条件路由、本地投影,或 reviewed unpinned remote adapter;不得用单个 `interface_ref` 冒充完整实现,且必须提供 reviewed reason。未来需要表达多个 RPC 时使用单独的复合接口模型。
- 任意合法 mode + `unavailable`:不得携带 `interface_ref`,必须提供明确 reason,并且 Agent 不得把它当作可用接口。
## 6. Schema、Help 与业务数据边界
| 问题 | 事实源 |
|---|---|
| 当前二进制是否暴露命令、Cobra 接受哪些 flags | `dws <path> --help` |
| Agent 选哪个命令、参数映射/required/约束、risk/confirmation | 对应 leaf `dws schema "<path>"` |
| 钉钉中的文档、文件、日程、消息等实际数据 | 真正执行 `dws doc read`、`dws drive search` 等 read/search/list 命令 |
Schema 和 Help 冲突是契约漂移,不能静默猜测:
- 执行参数以 Cobra 实际接受的 flags 为准;不要发送 Help 中不存在的 flag。
- 安全语义冲突时不要采用更宽松值。先按更保守的解释确认;如果无法确定安全执行方式,停止并报告漂移。
- Schema/Help 只完成命令发现和契约读取。需要业务结果时,必须继续执行真实 read/search/list 命令。
上述运行时漂移策略不改变构建期的 value-neutral precedence;前者是在契约已经互相矛盾时保护用户,后者是在确定性生成同一契约。
## 7. 查询投影
```bash
dws schema # 产品紧凑概览
dws schema calendar # 产品摘要
dws schema "calendar event" # 分组摘要
dws schema "calendar event create" # 完整 leaf
dws schema "calendar event create" --compact # 支持:裁掉 provenance/debug 字段
dws schema --all # 所有工具的完整 leaf 导出
```
`schema list` 是根概览的兼容入口。
`schema --all` 必须包含最终 `SchemaIndex` 中每个 tool 的完整 leaf 参数、约束和安全语义;无业务参数的命令也要包含空 `parameters` 对象。它用于审计、CI 和参数防丢 baseline,但输出很大,普通 Agent 命令发现不得使用,应按 overview -> product/group -> leaf 渐进查询。
`--compact` 当前受支持,适合减少常规 leaf 查询上下文。`schema --all --compact` 也可执行,但会移除 provenance/debug 和接口映射字段,不能作为完整兼容性 baseline。
兼容旧二进制时,如果 Schema 查询返回 `unknown_flag: --compact`,只去掉 `--compact` 重试同一个查询。这是展示能力降级,不代表 leaf 缺失,也不能改用 Schema 查询业务数据。
## 8. 生成与发布
当 Cobra、flag、identity、binding、manual hint、Agent hint 或 Skill 发生变化时:
1. 审核真实 Cobra 变化,确认命令和 flag 已实际存在。新增或修改稳定 command identity、primary CLI path 或 alias 时,精确编辑 reviewed `CommandRegistry`(当前持久化文件为 `schema_command_registry.json`)。参数、Skill 或 metadata 单独变化时不要机械改写 Registry,也不要从旧 Catalog 反向生成它。
2. 仅对明确例外使用 reviewed manual command addition;它必须精确引用现有 runnable leaf、带 reason,并在生成时归一化进 `EffectiveCommandRegistry`。Native identity annotation 若存在,应作为与 Registry 一致的实现断言维护,而不是用来 materialize identity。
3. 生成 Agent metadata:
```bash
make generate-schema-agent-metadata
```
4. 从统一 typed registry 生成最终 Catalog:
```bash
make generate-schema-catalog
```
也可以运行 `go generate ./internal/cli` 生成正常发布资产。生成文件包括:
- `internal/cli/schema_agent_metadata/index.json`
- `internal/cli/schema_agent_metadata/<product>.json`
- `internal/cli/schema_agent_metadata_audit.json`
- `internal/cli/schema_catalog.json`
只编辑来源;不要手工编辑 Agent metadata 或 Catalog 输出。
## 9. Completeness 与 final-delivery invariant
门禁必须验证最终交付对象,而不是某个中间层或数量:
- 每个 public runnable Cobra leaf 要么能通过最终 embedded `SchemaIndex` 查询,要么有 exact、reviewed、带 reason 的 exclusion。
- 每个最终 canonical path、primary CLI path 和 alias 都必须解析到同一个可执行 leaf;不得有 phantom path 或 collision。
- `EffectiveCommandRegistry`、`SchemaRegistry/SchemaIndex`、Agent metadata 和 Catalog canonical sets 必须精确一致,不能只比较 count。
- Leaf payload、`--all` 中对应 tool 和 Catalog full tool 必须是同一个 resolved `ToolSpec` 的内容级等价投影,并通过 production loader round-trip。
- overview/product/group summary 与 Catalog summary 必须等于同一个 `ToolSpec.ToSummaryPayload()`;alias 查询只允许 `cli_path` 和 `is_alias` 这两个视图字段变化。
- 每个最终字段及 parameter field 的 provenance winner value 必须与 delivered value 精确一致;不能只验证 provenance source、count 或字段是否存在。
- 每个 MCP `interface_ref` 必须在 pinned interface registry 精确存在;local/composite/unavailable 必须满足同一 conflict matrix。
- `--all` 的 tool set 必须与最终 index 一对一,且每个工具包含完整参数契约。
- 连续两次生成必须字节稳定,提交的生成物不得漂移。
推荐本地验证:
```bash
make generate-schema-agent-metadata
make generate-schema-catalog
./scripts/policy/check-generated-drift.sh
./scripts/policy/check-schema-catalog.sh
go test ./internal/cli ./internal/app ./internal/generator/... -count=1
```
## 10. 明确禁止
- 运行时调用 MCP `tools/list` 或访问网络生成 Schema。
- 从旧 `schema_catalog.json` 或其他 generated JSON 反向创建/补齐 Cobra leaf、flag、CommandRegistry 或下一轮 Catalog。
- 把 native annotation、legacy registry 或 Catalog 当作 identity fallback;或在 `EffectiveCommandRegistry` 之后再次选择 identity winner。
- renderer、query 或 gate 在 `SchemaRegistry` 之后重新读取 source 并做第二次 merge。
- 用 prefix/wildcard exclusion 隐藏未来命令。
- 让 manual hint、CommandRegistry 或 interface metadata 宣称一个不存在的命令、flag 或 RPC 可用。
- 把 `schema --all` 当作普通业务数据查询,或把其完整结果无条件注入 Agent 上下文。
+233
View File
@@ -0,0 +1,233 @@
package app
import (
"bufio"
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit"
"github.com/spf13/cobra"
)
func newAuditCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "audit",
Short: "操作审计日志管理",
Long: "查看、导出和校验本地操作审计日志。",
}
cmd.AddCommand(
newAuditTailCommand(),
newAuditExportCommand(),
newAuditVerifyCommand(),
)
return cmd
}
func newAuditTailCommand() *cobra.Command {
var n int
cmd := &cobra.Command{
Use: "tail",
Short: "查看最近的审计记录",
RunE: func(cmd *cobra.Command, args []string) error {
if n < 1 {
return fmt.Errorf("--lines 必须为正整数,收到 %d", n)
}
dir := auditDir()
file, err := audit.LatestAuditFile(dir)
if err != nil {
return fmt.Errorf("无审计记录: %w", err)
}
lines, err := tailFile(file, n)
if err != nil {
return err
}
for _, line := range lines {
fmt.Println(line)
}
return nil
},
}
cmd.Flags().IntVarP(&n, "lines", "n", 20, "显示最近 N 条记录")
return cmd
}
func newAuditExportCommand() *cobra.Command {
var since, until, format string
cmd := &cobra.Command{
Use: "export",
Short: "导出审计日志",
RunE: func(cmd *cobra.Command, args []string) error {
dir := auditDir()
sinceDate := strings.ReplaceAll(since, "-", "")
untilDate := strings.ReplaceAll(until, "-", "")
files, err := audit.AuditFilesInRange(dir, sinceDate, untilDate)
if err != nil {
return fmt.Errorf("查找审计文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("指定范围内无审计文件")
}
switch format {
case "jsonl":
return exportJSONL(files)
case "csv":
return exportCSV(files)
default:
return fmt.Errorf("不支持的格式: %s(可选 jsonl, csv)", format)
}
},
}
cmd.Flags().StringVar(&since, "since", "", "起始日期 (YYYY-MM-DD)")
cmd.Flags().StringVar(&until, "until", "", "截止日期 (YYYY-MM-DD)")
cmd.Flags().StringVar(&format, "format", "jsonl", "输出格式: jsonl 或 csv")
return cmd
}
func newAuditVerifyCommand() *cobra.Command {
var file string
cmd := &cobra.Command{
Use: "verify",
Short: "校验审计日志哈希链完整性",
RunE: func(cmd *cobra.Command, args []string) error {
target := file
if target == "" {
dir := auditDir()
var err error
target, err = audit.LatestAuditFile(dir)
if err != nil {
return fmt.Errorf("无审计文件: %w", err)
}
}
valid, brokenAt, err := audit.VerifyFile(target)
if err != nil {
return fmt.Errorf("校验失败: %w", err)
}
if valid {
fmt.Printf("✓ %s 哈希链完整(全部通过)\n", filepath.Base(target))
} else {
fmt.Printf("✗ %s 哈希链在第 %d 行断裂\n", filepath.Base(target), brokenAt)
os.Exit(1)
}
return nil
},
}
cmd.Flags().StringVar(&file, "file", "", "指定审计文件路径(默认最新文件)")
return cmd
}
func auditDir() string {
if dir := os.Getenv(audit.EnvAuditDir); dir != "" {
return dir
}
return filepath.Join(defaultConfigDir(), "audit")
}
func tailFile(path string, n int) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(lines) > n {
lines = lines[len(lines)-n:]
}
return lines, nil
}
func exportJSONL(files []string) error {
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
f.Close()
if err := scanner.Err(); err != nil {
return err
}
}
return nil
}
func exportCSV(files []string) error {
w := csv.NewWriter(os.Stdout)
header := []string{"timestamp", "execution_id", "user_id", "corp_id", "product", "command", "result", "duration_ms", "error_category"}
if err := w.Write(header); err != nil {
return fmt.Errorf("写入 CSV 表头失败: %w", err)
}
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
if len(bytes.TrimSpace(line)) == 0 {
continue
}
var evt audit.Event
if err := json.Unmarshal(line, &evt); err != nil {
f.Close()
return fmt.Errorf("解析审计记录失败 %s:%d: %w", file, lineNum, err)
}
row := []string{
evt.Timestamp.Format(time.RFC3339),
evt.ExecutionID,
evt.Actor.UserID,
evt.Actor.CorpID,
evt.Product,
evt.Command,
evt.Result,
strconv.FormatInt(evt.DurationMs, 10),
evt.ErrCategory,
}
if err := w.Write(row); err != nil {
f.Close()
return fmt.Errorf("写入 CSV 记录失败: %w", err)
}
}
if err := scanner.Err(); err != nil {
f.Close()
return err
}
f.Close()
}
w.Flush()
if err := w.Error(); err != nil {
return fmt.Errorf("刷新 CSV 输出失败: %w", err)
}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package app
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestAuditTailRejectsNonPositiveLines(t *testing.T) {
for _, n := range []string{"0", "-1"} {
cmd := newAuditTailCommand()
cmd.SetArgs([]string{"--lines", n})
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
if err == nil {
t.Fatalf("--lines %s: expected error, got nil", n)
}
if !strings.Contains(err.Error(), "正整数") {
t.Fatalf("--lines %s: unexpected error: %v", n, err)
}
}
}
func TestTailFileReturnsLastN(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit-20260101.jsonl")
if err := os.WriteFile(path, []byte("a\nb\nc\nd\ne\n"), 0o600); err != nil {
t.Fatal(err)
}
lines, err := tailFile(path, 2)
if err != nil {
t.Fatal(err)
}
if len(lines) != 2 || lines[0] != "d" || lines[1] != "e" {
t.Fatalf("got %v, want [d e]", lines)
}
}
func TestExportCSVWritesHeaderAndRows(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit-20260101.jsonl")
rec := `{"timestamp":"2026-01-01T00:00:00Z","execution_id":"e1","actor":{"user_id":"u1","corp_id":"c1"},"product":"calendar","command":"event_list","result":"success","duration_ms":12,"hash":"h","prev_hash":""}`
if err := os.WriteFile(path, []byte(rec+"\n"), 0o600); err != nil {
t.Fatal(err)
}
stdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
exportErr := exportCSV([]string{path})
w.Close()
os.Stdout = stdout
if exportErr != nil {
t.Fatalf("exportCSV error: %v", exportErr)
}
buf := make([]byte, 4096)
n, _ := r.Read(buf)
out := string(buf[:n])
if !strings.Contains(out, "timestamp,execution_id") {
t.Fatalf("missing CSV header, got: %q", out)
}
if !strings.Contains(out, "e1") || !strings.Contains(out, "event_list") {
t.Fatalf("missing CSV row data, got: %q", out)
}
}
// TestExportCSVFailsOnMalformedJSON guards the reviewer's V9 finding: a corrupt
// JSONL line must surface an error with file/line evidence instead of being
// silently skipped while the command exits 0.
func TestExportCSVFailsOnMalformedJSON(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit-20260101.jsonl")
good := `{"timestamp":"2026-01-01T00:00:00Z","execution_id":"e1","actor":{"user_id":"u1"},"product":"calendar","command":"event_list","result":"success","duration_ms":1,"hash":"h","prev_hash":""}`
if err := os.WriteFile(path, []byte(good+"\nnot-json\n"), 0o600); err != nil {
t.Fatal(err)
}
stdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
exportErr := exportCSV([]string{path})
w.Close()
os.Stdout = stdout
// Drain the pipe so the writer never blocks.
buf := make([]byte, 4096)
_, _ = r.Read(buf)
if exportErr == nil {
t.Fatal("expected error on malformed JSONL, got nil")
}
if !strings.Contains(exportErr.Error(), "解析审计记录失败") {
t.Fatalf("error missing parse context: %v", exportErr)
}
if !strings.Contains(exportErr.Error(), ":2") {
t.Fatalf("error missing line evidence: %v", exportErr)
}
}
+164
View File
@@ -0,0 +1,164 @@
package app
import (
"errors"
"fmt"
"os"
"runtime"
"sync"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
)
var (
auditSinkOnce sync.Once
auditCloseOnce sync.Once
sharedAuditSink audit.Sink
auditIDMu sync.Mutex
cachedActor audit.Actor
cachedAgentID string
cachedProfile string
identityLoaded bool
// loadTokenForProfile is the profile-scoped token loader. It is a package
// variable so profile-switch Actor attribution can be tested deterministically
// without touching the OS keychain.
loadTokenForProfile = auth.LoadTokenDataForProfile
)
// setupAuditSink builds the process-wide audit sink once and caches it so the
// runner and the shutdown hook share a single writer/forwarder instance.
func setupAuditSink() audit.Sink {
auditSinkOnce.Do(func() {
sink, err := audit.BuildSink(defaultConfigDir(), auditReport)
if err != nil {
auditReport("initialization failed, audit disabled for this session: %v", err)
sharedAuditSink = audit.NopSink{}
return
}
sharedAuditSink = sink
})
return sharedAuditSink
}
// CloseAuditSink flushes in-flight remote forwards and closes the audit writer.
// It is invoked from an unconditional defer in Execute so the drain happens for
// both successful and failed commands (Cobra skips PersistentPostRunE when RunE
// returns an error). The sync.Once makes repeated calls safe.
func CloseAuditSink() {
auditCloseOnce.Do(func() {
if sharedAuditSink == nil {
return
}
if err := sharedAuditSink.Close(); err != nil {
auditReport("close failed: %v", err)
}
})
}
// auditReport routes non-fatal audit-subsystem diagnostics to the structured
// file log (always, when available) and to stderr when DWS_AUDIT_DEBUG is set,
// so init/write/forward failures are observable instead of silently swallowed.
func auditReport(format string, args ...any) {
msg := "audit: " + fmt.Sprintf(format, args...)
if l := FileLoggerInstance(); l != nil {
l.Warn(msg)
}
if audit.DebugEnabled() {
fmt.Fprintln(os.Stderr, "[dws] "+msg)
}
}
// auditIdentity resolves the Actor for the active runtime profile. The result
// is cached per-profile so a profile switch within a long-running process (e.g.
// serve mode) re-resolves rather than reusing a stale identity.
func auditIdentity() (audit.Actor, string) {
profile := auth.RuntimeProfile()
auditIDMu.Lock()
defer auditIDMu.Unlock()
if identityLoaded && profile == cachedProfile {
return cachedActor, cachedAgentID
}
configDir := defaultConfigDir()
var actor audit.Actor
if td, err := loadTokenForProfile(configDir, profile); err == nil && td != nil {
actor = audit.Actor{
UserID: td.UserID,
Name: td.UserName,
CorpID: td.CorpID,
CorpName: td.CorpName,
}
} else if err != nil {
auditReport("resolve actor for profile %q failed: %v", profile, err)
}
agentID := ""
if id := auth.Load(configDir); id != nil {
agentID = id.AgentID
}
cachedActor, cachedAgentID, cachedProfile, identityLoaded = actor, agentID, profile, true
return actor, agentID
}
func emitAudit(sink audit.Sink, execID string, invokeStart time.Time, invocation executor.Invocation, endpoint string, retErr error, cliVersion string) {
if sink == nil {
return
}
if _, ok := sink.(audit.NopSink); ok {
return
}
actor, agentID := auditIdentity()
result := "success"
var errCat, errReason string
if retErr != nil {
result = "error"
errCat, errReason = classifyAuditError(retErr)
}
paramsSummary := logging.SanitizeArguments(invocation.Params, 1024)
evt := &audit.Event{
Timestamp: invokeStart,
ExecutionID: execID,
AgentID: agentID,
Actor: actor,
Product: invocation.CanonicalProduct,
Command: invocation.Tool,
Endpoint: transport.RedactURL(endpoint),
ParamsSummary: paramsSummary,
Result: result,
ErrCategory: errCat,
ErrReason: errReason,
DurationMs: time.Since(invokeStart).Milliseconds(),
CLIVersion: cliVersion,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
if err := sink.Emit(evt); err != nil {
auditReport("emit event failed (exec %s): %v", execID, err)
}
}
func classifyAuditError(err error) (category, reason string) {
if err == nil {
return "", ""
}
var typed *apperrors.Error
if errors.As(err, &typed) {
return string(typed.Category), typed.Reason
}
return "unknown", err.Error()
}
+131
View File
@@ -0,0 +1,131 @@
package app
import (
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
)
// TestAuditIdentityReresolvesOnProfileSwitch guards the reviewer's finding that a
// long-running process (e.g. serve mode) must attribute events to the ACTIVE
// runtime profile rather than reusing a process-global first Actor. It also
// asserts the per-profile cache avoids redundant token loads within one profile.
func TestAuditIdentityReresolvesOnProfileSwitch(t *testing.T) {
prevLoader := loadTokenForProfile
prevProfile := auth.RuntimeProfile()
t.Cleanup(func() {
loadTokenForProfile = prevLoader
auth.SetRuntimeProfile(prevProfile)
resetAuditIdentityCache()
})
resetAuditIdentityCache()
var mu sync.Mutex
calls := map[string]int{}
loadTokenForProfile = func(_ /*configDir*/, profile string) (*auth.TokenData, error) {
mu.Lock()
calls[profile]++
mu.Unlock()
switch profile {
case "orgA":
return &auth.TokenData{UserID: "ua", UserName: "Alice", CorpID: "ca", CorpName: "CorpA"}, nil
case "orgB":
return &auth.TokenData{UserID: "ub", UserName: "Bob", CorpID: "cb", CorpName: "CorpB"}, nil
default:
return nil, nil
}
}
auth.SetRuntimeProfile("orgA")
if actor, _ := auditIdentity(); actor.UserID != "ua" || actor.CorpName != "CorpA" {
t.Fatalf("orgA: got %+v, want Alice/CorpA", actor)
}
// Second call under the same profile must hit the cache (no extra load).
if actor, _ := auditIdentity(); actor.UserID != "ua" {
t.Fatalf("orgA cached: got %+v", actor)
}
auth.SetRuntimeProfile("orgB")
if actor, _ := auditIdentity(); actor.UserID != "ub" || actor.CorpName != "CorpB" {
t.Fatalf("orgB: got %+v, want Bob/CorpB (stale Actor reused?)", actor)
}
mu.Lock()
defer mu.Unlock()
if calls["orgA"] != 1 {
t.Fatalf("orgA loaded %d times, want 1 (cache miss?)", calls["orgA"])
}
if calls["orgB"] != 1 {
t.Fatalf("orgB loaded %d times, want 1", calls["orgB"])
}
}
func resetAuditIdentityCache() {
auditIDMu.Lock()
defer auditIDMu.Unlock()
cachedActor = audit.Actor{}
cachedAgentID = ""
cachedProfile = ""
identityLoaded = false
}
// TestCloseAuditSinkDrainsOnErrorPath guards the reviewer's V5 finding: when a
// command's RunE returns an error, Cobra skips PersistentPostRunE, so the audit
// drain must instead happen through the unconditional defer in Execute that calls
// CloseAuditSink. This test wires a real forwarder-backed sink into the shared
// slot and asserts CloseAuditSink flushes the queued forward exactly as the
// error-path defer would, and that a second call is a harmless no-op.
func TestCloseAuditSinkDrainsOnErrorPath(t *testing.T) {
var delivered int64
var releaseOnce sync.Once
release := make(chan struct{})
releaseFn := func() { releaseOnce.Do(func() { close(release) }) }
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release // hold the request until the drain awaits it
atomic.AddInt64(&delivered, 1)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
defer releaseFn() // LIFO: unblock any in-flight handler before srv.Close()
writer, err := audit.NewDateRotatingWriter(t.TempDir(), 0)
if err != nil {
t.Fatal(err)
}
fwd := audit.NewHTTPForwarder(srv.URL, "", audit.RedactNone, nil)
sink := audit.NewFileSink(writer, audit.NewChain(""), fwd)
prevSink := sharedAuditSink
t.Cleanup(func() {
sharedAuditSink = prevSink
auditCloseOnce = sync.Once{}
})
sharedAuditSink = sink
auditCloseOnce = sync.Once{}
if err := sink.Emit(&audit.Event{Timestamp: time.Unix(0, 0), Product: "calendar", Command: "event_list", Result: "error"}); err != nil {
t.Fatalf("emit: %v", err)
}
if got := atomic.LoadInt64(&delivered); got != 0 {
t.Fatalf("forward delivered before drain: %d", got)
}
// Let the held request complete, then drain via the same entry point the
// error-path defer uses. CloseAuditSink blocks until the forward goroutine
// observes the HTTP response, so the counter is settled when it returns.
releaseFn()
CloseAuditSink()
if got := atomic.LoadInt64(&delivered); got != 1 {
t.Fatalf("forward not drained on error path: delivered=%d, want 1", got)
}
// Idempotent: the success-path PersistentPostRunE and the defer both call it.
CloseAuditSink()
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
)
func TestToolCallerAdapterDryRunNeverInvokesRunner(t *testing.T) {
runner := &countingErrorRunner{}
caller := newToolCallerAdapter(runner, &GlobalFlags{DryRun: true, Format: "json"})
result, err := caller.CallTool(context.Background(), "aitable-helper", "set_advanced_permission", map[string]any{"enabled": false})
if err != nil {
t.Fatalf("CallTool() error = %v", err)
}
if got := runner.calls.Load(); got != 0 {
t.Fatalf("runner calls = %d, want 0", got)
}
if result == nil || len(result.Content) != 1 || !strings.Contains(result.Content[0].Text, `"dry_run":true`) {
t.Fatalf("dry-run result = %#v", result)
}
var nilAdapter *toolCallerAdapter
if nilAdapter.DryRun() || nilAdapter.Format() != "json" {
t.Fatal("nil adapter accessors are not safe")
}
if _, err := nilAdapter.CallTool(context.Background(), "x", "y", nil); err == nil {
t.Fatal("nil adapter accepted a tool call")
}
}
func TestRuntimeRunnerGlobalDryRunStopsBeforeInjectedFallback(t *testing.T) {
fallback := &countingErrorRunner{}
runner := &runtimeRunner{globalFlags: &GlobalFlags{DryRun: true}, fallback: fallback}
result, err := runner.Run(context.Background(), executor.NewHelperInvocation(
"test",
"aitable",
"tool",
map[string]any{"id": "x"},
))
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if !result.Invocation.DryRun || result.Response["dry_run"] != true {
t.Fatalf("dry-run result = %#v", result)
}
if got := fallback.calls.Load(); got != 0 {
t.Fatalf("fallback calls = %d, want 0", got)
}
}
type countingErrorRunner struct {
calls atomic.Int64
}
func (r *countingErrorRunner) Run(context.Context, executor.Invocation) (executor.Result, error) {
r.calls.Add(1)
return executor.Result{}, errors.New("runner must not be called")
}
+72 -2
View File
@@ -30,6 +30,8 @@ import (
"time"
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/bus"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/busctl"
@@ -103,7 +105,8 @@ func newEventConsumeCommand() *cobra.Command {
默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加
--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用
SIGTERM、关 stdin,或 dws event stop <subscribe_id>,不要 kill -9。
SIGTERM、关 stdin,或先用 dws event stop <subscribe_id> --dry-run 预览、确认后加
--yes,绝不要 kill -9。
--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费
通常不需要设置。`,
Args: cobra.MaximumNArgs(1),
@@ -283,7 +286,7 @@ SIGTERM、关 stdin,或 dws event stop <subscribe_id>,不要 kill -9。
f.BoolVar(&personalOpts.Ephemeral, "ephemeral", false,
"强制退出时取消个人订阅。默认已按归属清理:本次新建的订阅退出即取消,"+
"用 --subscribe-id 复用的订阅保留。优雅停可用 SIGTERM、关闭 stdin,"+
"或从外部 dws event stop <subscribe_id>(会一并退订);"+
"或从外部先用 dws event stop <subscribe_id> --dry-run 预览、确认后加 --yes(会一并退订);"+
"请勿 kill -9(会跳过退订、泄漏服务端订阅)")
f.StringVar(&personalOpts.UserID, "user", "",
"个人单聊对端 userId")
@@ -300,6 +303,13 @@ SIGTERM、关 stdin,或 dws event stop <subscribe_id>,不要 kill -9。
f.StringVar(&streamOpts.TicketURL, "stream-ticket-url", strings.TrimSpace(os.Getenv("DWS_STREAM_TICKET_URL")),
"个人 Stream 取票 URL;默认由 MCP base URL 派生")
hideEventInternalFlags(cmd, "as")
cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{
Name: "event_key",
Type: "string",
Description: "要消费的个人事件码;省略时仅适用于显式配置其它事件来源的兼容模式",
Required: false,
Index: 0,
})
return cmd
}
@@ -985,6 +995,19 @@ func newEventStopCommand() *cobra.Command {
}
if as == "user" {
opts.SubscribeID = firstArg(args)
hasSubscribeID := strings.TrimSpace(opts.SubscribeID) != ""
if hasSubscribeID && opts.All {
return fmt.Errorf("event stop --as user: subscribe_id and --all are mutually exclusive")
}
if !hasSubscribeID && !opts.All {
return fmt.Errorf("event stop --as user: subscribe_id is required unless --all is set")
}
if eventStopDryRun(c) {
return writeEventStopDryRun(c, as, opts)
}
if !eventStopConfirmed(c) {
return eventStopConfirmationRequired("event stop 会取消个人事件订阅并停止本地消费")
}
return runPersonalEventStop(c, opts)
}
if err := rejectChangedFlags(c, "user", "all", "personal-event-base-url", "stream-source-id"); err != nil {
@@ -993,6 +1016,12 @@ func newEventStopCommand() *cobra.Command {
if len(args) > 0 {
return fmt.Errorf("event stop: subscribe_id is only supported with --as user")
}
if eventStopDryRun(c) {
return writeEventStopDryRun(c, as, opts)
}
if !eventStopConfirmed(c) {
return eventStopConfirmationRequired("event stop 会停止事件消费")
}
configDir := defaultConfigDir()
clientID, _, _, _, err := authpkg.ResolveAppCredentialsStrict(configDir)
if err != nil {
@@ -1018,9 +1047,50 @@ func newEventStopCommand() *cobra.Command {
"个人事件 sourceId;开源版默认 open,可由 edition 覆盖")
cmd.Flags().BoolVar(&opts.All, "all", false, "取消当前身份下本地记录的所有个人订阅")
hideEventInternalFlags(cmd, "as")
cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{
Name: "subscribe_id",
Type: "string",
Description: "要取消的个人事件订阅 ID;与 --all 二选一",
Required: false,
Index: 0,
})
return cmd
}
func eventStopDryRun(cmd *cobra.Command) bool {
value, _ := cmd.Flags().GetBool("dry-run")
return value
}
func eventStopConfirmed(cmd *cobra.Command) bool {
value, _ := cmd.Flags().GetBool("yes")
return value
}
func eventStopConfirmationRequired(action string) error {
return apperrors.NewValidation(
action+";请先使用 --dry-run 预览,确认后加 --yes 执行",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先以相同参数加 --dry-run 预览;获得用户确认后改用 --yes 执行"),
apperrors.WithActions("使用 --dry-run 生成预览", "获得用户确认后使用 --yes 执行"),
)
}
func writeEventStopDryRun(cmd *cobra.Command, identity string, opts personalStopOptions) error {
payload := map[string]any{
"dry_run": true,
"action": "event.stop",
"identity": strings.TrimSpace(identity),
"all": opts.All,
}
if subscribeID := strings.TrimSpace(opts.SubscribeID); subscribeID != "" {
payload["subscribe_id"] = subscribeID
}
encoder := json.NewEncoder(cmd.OutOrStdout())
encoder.SetIndent("", " ")
return encoder.Encode(payload)
}
// ─────────────────────────────────────────────────────────────────────
// helpers
// ─────────────────────────────────────────────────────────────────────
+8
View File
@@ -30,6 +30,7 @@ import (
"time"
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/bus"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/busctl"
@@ -135,6 +136,13 @@ func newEventSchemaCommand() *cobra.Command {
cmd.Flags().StringVar(&asIdentity, "as", "user", "事件身份: user")
cmd.Flags().StringVarP(&formatRaw, "format", "f", "json", "输出格式: json")
hideEventInternalFlags(cmd, "as")
cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{
Name: "event_key",
Type: "string",
Description: "要查询 payload 字段定义的个人事件码",
Required: true,
Index: 0,
})
return cmd
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"sort"
"testing"
"github.com/spf13/cobra"
)
func TestEventCommandRemainsVisibleAsBuiltInPublicGroup(t *testing.T) {
root := &cobra.Command{Use: "dws"}
event := newEventCommand()
unregistered := &cobra.Command{Use: "unregistered", Run: func(*cobra.Command, []string) {}}
root.AddCommand(event, unregistered)
hideNonDirectRuntimeCommands(root)
if event.Hidden {
t.Fatal("built-in event command was hidden by the direct-runtime visibility filter")
}
if !unregistered.Hidden {
t.Fatal("control command outside the built-in/direct-runtime sets remained visible")
}
var leaves []string
for _, command := range event.Commands() {
if command.Hidden || !command.Runnable() {
continue
}
leaves = append(leaves, command.Name())
}
sort.Strings(leaves)
want := []string{"consume", "list", "schema", "status", "stop"}
if len(leaves) != len(want) {
t.Fatalf("public event leaves = %v, want %v", leaves, want)
}
for index := range want {
if leaves[index] != want[index] {
t.Fatalf("public event leaves = %v, want %v", leaves, want)
}
}
}
func TestPluginCannotReplaceBuiltInEventCommand(t *testing.T) {
root := &cobra.Command{Use: "dws"}
builtIn := newEventCommand()
root.AddCommand(builtIn)
pluginEvent := &cobra.Command{Use: "event", Run: func(*cobra.Command, []string) {}}
addPluginCommandsSafe(root, []*cobra.Command{pluginEvent})
var eventCommands []*cobra.Command
for _, command := range root.Commands() {
if command.Name() == "event" {
eventCommands = append(eventCommands, command)
}
}
if len(eventCommands) != 1 || eventCommands[0] != builtIn {
t.Fatalf("event command after plugin registration = %p (%d matches), want built-in %p", firstEventCommand(eventCommands), len(eventCommands), builtIn)
}
if pluginEvent.Parent() != nil {
t.Fatal("conflicting plugin event command was attached to the root")
}
}
func firstEventCommand(commands []*cobra.Command) *cobra.Command {
if len(commands) == 0 {
return nil
}
return commands[0]
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
func TestEventStopRequiresTypedConfirmationBeforeMutation(t *testing.T) {
root, _ := newEventStopSafetyRoot()
root.SetArgs([]string{"event", "stop", "sub-1"})
err := root.Execute()
if err == nil {
t.Fatal("event stop without --yes or --dry-run unexpectedly succeeded")
}
var appErr *apperrors.Error
if !errors.As(err, &appErr) || appErr.Category != apperrors.CategoryValidation {
t.Fatalf("event stop confirmation error = %T %v, want typed validation error", err, err)
}
if appErr.Reason != "confirmation_required" {
t.Fatalf("event stop confirmation reason = %q, want confirmation_required", appErr.Reason)
}
for _, recoveryFlag := range []string{"--dry-run", "--yes"} {
if !strings.Contains(err.Error(), recoveryFlag) {
t.Fatalf("event stop confirmation error %q does not explain %s", err, recoveryFlag)
}
}
}
func TestEventStopDryRunPrecedesConfirmationAndReturnsPreview(t *testing.T) {
tests := []struct {
name string
args []string
wantAll bool
wantSubscribeID string
}{
{name: "single subscription", args: []string{"event", "stop", "sub-1", "--dry-run"}, wantSubscribeID: "sub-1"},
{name: "all subscriptions", args: []string{"--dry-run", "event", "stop", "--all"}, wantAll: true},
{name: "dry run wins over yes", args: []string{"event", "stop", "sub-2", "--yes", "--dry-run"}, wantSubscribeID: "sub-2"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
root, stdout := newEventStopSafetyRoot()
root.SetArgs(test.args)
if err := root.Execute(); err != nil {
t.Fatalf("event stop dry-run error = %v", err)
}
var preview map[string]any
if err := json.Unmarshal(stdout.Bytes(), &preview); err != nil {
t.Fatalf("decode event stop dry-run preview: %v\n%s", err, stdout.String())
}
if preview["dry_run"] != true || preview["action"] != "event.stop" || preview["identity"] != "user" {
t.Fatalf("event stop dry-run preview = %#v", preview)
}
if got, _ := preview["all"].(bool); got != test.wantAll {
t.Fatalf("event stop dry-run all = %v, want %v", got, test.wantAll)
}
if got, _ := preview["subscribe_id"].(string); got != test.wantSubscribeID {
t.Fatalf("event stop dry-run subscribe_id = %q, want %q", got, test.wantSubscribeID)
}
})
}
}
func TestEventStopDryRunDoesNotBypassTargetValidation(t *testing.T) {
for _, test := range []struct {
name string
args []string
want string
}{
{name: "missing target", args: []string{"event", "stop", "--dry-run"}, want: "subscribe_id is required unless --all is set"},
{name: "conflicting targets", args: []string{"event", "stop", "sub-1", "--all", "--dry-run"}, want: "subscribe_id and --all are mutually exclusive"},
} {
t.Run(test.name, func(t *testing.T) {
root, _ := newEventStopSafetyRoot()
root.SetArgs(test.args)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("event stop dry-run validation error = %v, want %q", err, test.want)
}
})
}
}
func newEventStopSafetyRoot() (*cobra.Command, *bytes.Buffer) {
stdout := &bytes.Buffer{}
root := &cobra.Command{
Use: "dws",
SilenceErrors: true,
SilenceUsage: true,
}
root.SetOut(stdout)
root.SetErr(&bytes.Buffer{})
root.PersistentFlags().Bool("dry-run", false, "preview without executing")
root.PersistentFlags().Bool("yes", false, "confirm execution")
event := &cobra.Command{Use: "event"}
event.AddCommand(newEventStopCommand())
root.AddCommand(event)
return root, stdout
}
-136
View File
@@ -1,136 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"context"
"fmt"
"sync"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
)
// newHelperToolFetcher returns a cli.HelperToolFetcher that loads a helper MCP
// server's tools/list LIVE (by source) and projects each tool into a
// cli.HelperToolSchema (name, description, inputSchema properties/required). It
// is injected into the schema command so the cli package can render
// `dws schema dev.*` from real server schema without importing app/transport.
//
// Sources: "op-app" backs the dev app commands (pinned endpoint); "devdoc"
// backs `dws dev doc search` (endpoint resolved dynamically, see
// helperSourceEndpoint). Results are memoized per source per process so
// repeated `dws schema dev.*` hit the network at most once per source. A failed
// fetch is not cached, allowing a later retry within the same process.
func newHelperToolFetcher() cli.HelperToolFetcher {
var (
mu sync.Mutex
cached = map[string]map[string]cli.HelperToolSchema{}
)
return func(ctx context.Context, source string) (map[string]cli.HelperToolSchema, error) {
mu.Lock()
if got, ok := cached[source]; ok {
mu.Unlock()
return got, nil
}
mu.Unlock()
endpoint, err := helperSourceEndpoint(source)
if err != nil {
return nil, err
}
schemas, err := fetchHelperToolSchemas(ctx, endpoint)
if err != nil {
return nil, err
}
mu.Lock()
cached[source] = schemas
mu.Unlock()
return schemas, nil
}
}
// helperSourceEndpoint maps a schema source to its MCP endpoint. op-app (dev
// app) is pinned in source (devappMCPEndpoint, derived from the active gateway
// base — production by default, pre when ~/.dws/mcp_url points at pre); other
// sources (e.g. devdoc) are resolved the same way the runner resolves a product
// endpoint — env override → discovery → edition StaticServers/SupplementServers.
func helperSourceEndpoint(source string) (string, error) {
switch source {
case "", "op-app", "devapp":
return devappMCPEndpoint(), nil
default:
if endpoint, ok := directRuntimeEndpoint(source, ""); ok {
return endpoint, nil
}
return "", fmt.Errorf("no MCP endpoint resolved for source %q (not injected by edition/discovery)", source)
}
}
// fetchHelperToolSchemas performs the live tools/list call against endpoint and
// converts the descriptors. Auth and identity headers are resolved the same way
// the runner does for direct-runtime invocations.
func fetchHelperToolSchemas(ctx context.Context, endpoint string) (map[string]cli.HelperToolSchema, error) {
token := resolveRuntimeAuthToken(ctx, "")
headers := resolveIdentityHeaders()
client := transport.NewClient(nil).WithAuth(token, headers)
result, err := client.ListTools(ctx, endpoint)
if err != nil {
return nil, err
}
out := make(map[string]cli.HelperToolSchema, len(result.Tools))
for _, td := range result.Tools {
out[td.Name] = cli.HelperToolSchema{
Name: td.Name,
Description: td.Description,
Properties: inputSchemaProperties(td.InputSchema),
Required: inputSchemaRequired(td.InputSchema),
}
}
return out, nil
}
// inputSchemaProperties pulls the "properties" object out of a deserialized
// MCP inputSchema map. Returns an empty (non-nil) map when absent.
func inputSchemaProperties(schema map[string]any) map[string]any {
if schema == nil {
return map[string]any{}
}
props, _ := schema["properties"].(map[string]any)
if props == nil {
return map[string]any{}
}
return props
}
// inputSchemaRequired pulls the "required" string list out of a deserialized
// MCP inputSchema map.
func inputSchemaRequired(schema map[string]any) []string {
if schema == nil {
return nil
}
raw, ok := schema["required"].([]any)
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, v := range raw {
if s, ok := v.(string); ok && s != "" {
out = append(out, s)
}
}
return out
}
+4
View File
@@ -129,6 +129,10 @@ func TestPrintPatAuthError_HumanReadable(t *testing.T) {
func TestPrintPatAuthJSON_MachineReadable(t *testing.T) {
t.Setenv(authpkg.AgentCodeEnv, "")
// PAT browser policy is user-configurable. Isolate the config directory so
// this serializer test exercises the built-in CLI-owned default instead of
// inheriting the developer's ~/.dws/pat_policy.json.
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
var buf strings.Builder
scopeErr := &PatScopeError{
Identity: "user",
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"sync/atomic"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/plugin"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes"
)
func isolatePluginRuntime(t *testing.T) {
t.Helper()
dynamicMu.Lock()
previousEndpoints := dynamicEndpoints
previousProducts := dynamicProducts
previousAliases := dynamicAliases
previousToolEndpoints := dynamicToolEndpoints
dynamicEndpoints = nil
dynamicProducts = nil
dynamicAliases = nil
dynamicToolEndpoints = nil
dynamicMu.Unlock()
stdioMu.Lock()
previousStdio := stdioClients
stdioClients = make(map[string]*transport.StdioClient)
stdioMu.Unlock()
t.Cleanup(func() {
StopAllStdioClients()
dynamicMu.Lock()
dynamicEndpoints = previousEndpoints
dynamicProducts = previousProducts
dynamicAliases = previousAliases
dynamicToolEndpoints = previousToolEndpoints
dynamicMu.Unlock()
stdioMu.Lock()
stdioClients = previousStdio
stdioMu.Unlock()
})
}
func TestRegisterPluginHTTPServerDoesNotProbeEndpoint(t *testing.T) {
isolatePluginRuntime(t)
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
calls.Add(1)
}))
defer server.Close()
registerPluginHTTPServer(mcptypes.ServerDescriptor{
Key: "offline-http",
Endpoint: server.URL,
CLI: mcptypes.CLIOverlay{
ID: "offline-http",
Command: "offline-http",
},
})
if got := calls.Load(); got != 0 {
t.Fatalf("plugin endpoint calls during registration = %d, want 0", got)
}
if endpoint, ok := directRuntimeEndpoint("offline-http", ""); !ok || endpoint != server.URL {
t.Fatalf("registered endpoint = (%q, %v), want (%q, true)", endpoint, ok, server.URL)
}
}
func TestRegisterStdioServerFromManifestDoesNotStartProcess(t *testing.T) {
isolatePluginRuntime(t)
marker := t.TempDir() + "/started"
client := transport.NewStdioClient("/bin/sh", []string{
"-c", fmt.Sprintf("printf started > %q", marker),
}, nil)
p := &plugin.Plugin{
Manifest: plugin.Manifest{Name: "lazy-stdio", Description: "lazy stdio test"},
Root: t.TempDir(),
}
descriptor := registerStdioServerFromManifest(p, plugin.StdioServerClient{Key: "local", Client: client})
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("stdio process started during registration: stat error = %v", err)
}
if descriptor.Endpoint != StdioEndpoint("lazy-stdio", "local") {
t.Fatalf("descriptor endpoint = %q", descriptor.Endpoint)
}
if _, ok := LookupStdioClient("lazy-stdio/local"); !ok {
t.Fatal("stdio client was not registered for lazy execution")
}
}
+6 -25
View File
@@ -19,10 +19,8 @@ import (
"os"
"path/filepath"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/plugin"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes"
"github.com/spf13/cobra"
)
// resolveStdioOverlay resolves the CLIOverlay for a stdio plugin server
@@ -73,25 +71,11 @@ func resolveStdioOverlay(p *plugin.Plugin, sc plugin.StdioServerClient) mcptypes
return overlay
}
// registerStdioServerFromOverlay builds cobra commands for a stdio plugin
// server using only its manifest + overlay.json.
//
// Returns (cmds, descriptor, true) when the overlay carries toolOverrides,
// otherwise (nil, zero, false) so the caller can fall back to discovery-first
// registration (legacy path).
//
// Dynamic command building has been removed; this now simply registers the
// server descriptor and returns nil commands.
func registerStdioServerFromOverlay(
p *plugin.Plugin,
sc plugin.StdioServerClient,
runner executor.Runner,
) ([]*cobra.Command, mcptypes.ServerDescriptor, bool) {
// registerStdioServerFromManifest registers an endpoint descriptor and an
// unstarted client from versioned plugin metadata. Tool discovery is not part
// of command-tree construction; execution starts and initializes the client.
func registerStdioServerFromManifest(p *plugin.Plugin, sc plugin.StdioServerClient) mcptypes.ServerDescriptor {
overlay := resolveStdioOverlay(p, sc)
if len(overlay.ToolOverrides) == 0 {
return nil, mcptypes.ServerDescriptor{}, false
}
descriptor := mcptypes.ServerDescriptor{
Key: sc.Key,
DisplayName: p.Manifest.Name + "/" + sc.Key,
@@ -105,11 +89,8 @@ func registerStdioServerFromOverlay(
AppendDynamicServer(descriptor)
RegisterStdioClient(p.Manifest.Name+"/"+sc.Key, sc.Client)
slog.Debug("plugin: stdio server registered from overlay",
slog.Debug("plugin: stdio server registered from manifest",
"plugin", p.Manifest.Name, "server", sc.Key,
"toolOverrides", len(overlay.ToolOverrides))
// Dynamic command tree building has been removed.
_ = runner
return nil, descriptor, true
return descriptor
}
+22 -285
View File
@@ -24,7 +24,6 @@ import (
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
@@ -39,7 +38,6 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline/handlers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/plugin"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/recovery"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes"
@@ -66,6 +64,8 @@ func Execute() (exitCode int) {
timing := NewTimingCollector()
defer func() {
StopAllStdioClients() // Ensure child processes are terminated on exit
CloseAuditSink() // Drain async audit forwards on all exit paths,
// including command errors where Cobra skips PersistentPostRunE.
timing.PrintIfEnabled()
timing.WriteReportIfEnabled(RawVersion(), SanitizeCommand(os.Args))
}()
@@ -335,6 +335,7 @@ func NewRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine)
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
StopAllStdioClients()
CloseAuditSink()
CloseFileLogger()
return closeOutputSink(cmd)
},
@@ -357,6 +358,7 @@ func NewRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine)
newConfigCommand(),
newDoctorCommand(),
newEventCommand(),
newAuditCommand(),
newCompletionCommand(root),
newRecoveryCommand(rootCtx, loader, flags),
newUpgradeCommand(),
@@ -385,7 +387,6 @@ func NewRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine)
fn(root, caller)
deduplicateCommands(root)
}
hideNonDirectRuntimeCommands(root)
configureRootHelp(root)
// Set custom flag error handler for better UX
@@ -539,7 +540,7 @@ func newVersionCommand() *cobra.Command {
}
func newSchemaCommand(loader cli.CatalogLoader) *cobra.Command {
return cli.NewSchemaCommand(loader, newHelperToolFetcher())
return cli.NewSchemaCommand(loader)
}
// buildMCPCommandFn is a test seam for newMCPCommand.
@@ -560,10 +561,12 @@ func hideNonDirectRuntimeCommands(root *cobra.Command) {
staticCommands := map[string]bool{
"auth": true,
"api": true,
"audit": true,
"cache": true,
"config": true,
"dev": true,
"doctor": true,
"event": true,
"completion": true,
"skill": true,
"plugin": true,
@@ -594,9 +597,9 @@ func hideNonDirectRuntimeCommands(root *cobra.Command) {
// not override. This protects core CLI functionality from being hijacked
// by a malicious or misconfigured plugin.
var reservedCommands = map[string]bool{
"auth": true, "api": true, "login": true, "logout": true,
"auth": true, "api": true, "audit": true, "login": true, "logout": true,
"plugin": true, "profile": true, "skill": true, "cache": true,
"config": true, "doctor": true, "completion": true,
"config": true, "doctor": true, "event": true, "completion": true,
"recovery": true, "upgrade": true, "version": true,
"schema": true, "mcp": true, "help": true,
}
@@ -668,43 +671,6 @@ func deduplicateCommands(root *cobra.Command) {
}
}
// pluginColdTimeouts holds the cold-path discovery budget for plugin MCP
// servers. Timeouts only apply to the *first* discovery for a given
// plugin/server; subsequent startups take the warm cache path and bypass
// the network entirely.
type pluginColdTimeouts struct {
httpNoAuth time.Duration
httpAuth time.Duration
stdio time.Duration
}
// resolvePluginColdTimeouts returns the cold-discovery budget for plugin MCP
// servers, applying the DWS_PLUGIN_COLD_TIMEOUT override when set. Defaults
// are tuned so healthy cross-region HTTP endpoints succeed on a cold start
// and Python/Node-based stdio plugins have headroom for interpreter load,
// while an unreachable host still surrenders in bounded time.
func resolvePluginColdTimeouts() pluginColdTimeouts {
t := pluginColdTimeouts{
httpNoAuth: 1 * time.Second,
httpAuth: 1500 * time.Millisecond,
stdio: 2 * time.Second,
}
raw := strings.TrimSpace(os.Getenv(cli.PluginColdTimeoutEnv))
if raw == "" {
return t
}
d, err := time.ParseDuration(raw)
if err != nil || d <= 0 {
slog.Warn("plugin: ignoring invalid DWS_PLUGIN_COLD_TIMEOUT",
"value", raw, "error", err)
return t
}
t.httpNoAuth = d
t.httpAuth = d
t.stdio = d
return t
}
func configureOutputSink(cmd *cobra.Command) error {
if local := cmd.LocalFlags().Lookup("output"); local != nil {
return nil
@@ -799,11 +765,10 @@ func CloseFileLogger() {
}
}
// loadPlugins scans plugin directories, injects their MCP servers into
// the dynamic server registry, and registers their pipeline hooks.
// This runs before legacy command construction so that plugin servers
// are available for EnvironmentLoader.Load().
func loadPlugins(engine *pipeline.Engine, runner executor.Runner) []*cobra.Command {
// loadPlugins registers versioned plugin manifests, stdio clients, hooks, and
// skills. It deliberately does not initialize MCP transports or call
// tools/list while constructing the command tree.
func loadPlugins(engine *pipeline.Engine, _ executor.Runner) []*cobra.Command {
pluginLoader := plugin.NewLoader(RawVersion())
// 0a. Inject plugin config values from settings.json as environment
@@ -833,96 +798,21 @@ func loadPlugins(engine *pipeline.Engine, runner executor.Runner) []*cobra.Comma
allPlugins := append(userPlugins, devPlugins...)
// 3. Discover tools from streamable-http servers and build CLI commands.
// Third-party servers with auth headers are discovered in parallel
// to avoid sequential 10s timeouts when multiple remote servers exist.
var pluginCmds []*cobra.Command
tc := transport.NewClient(nil)
// Collect all server descriptors and register auth first (fast, no I/O).
type pluginServer struct {
plugin *plugin.Plugin
srv mcptypes.ServerDescriptor
}
var httpServers []pluginServer
// 3. Register HTTP descriptors and authentication from the manifest.
for _, p := range allPlugins {
for _, srv := range p.ToServerDescriptors() {
AppendDynamicServer(srv)
if len(srv.AuthHeaders) > 0 {
registerPluginAuthFromHeaders(srv)
}
if srv.HasCLIMeta {
httpServers = append(httpServers, pluginServer{plugin: p, srv: srv})
}
registerPluginHTTPServer(srv)
}
}
// Collect all stdio clients up front so HTTP + stdio discovery can run
// concurrently — the slowest plugin (typically an unreachable HTTP
// endpoint hitting its dial timeout) dominates the parallel wall-clock,
// not the sum of every plugin's cold timeout.
type stdioEntry struct {
plugin *plugin.Plugin
sc plugin.StdioServerClient
}
var stdioEntries []stdioEntry
// 4. Register stdio descriptors and unstarted clients. The subprocess is
// started and initialized only when a command is actually executed.
for _, p := range allPlugins {
for _, sc := range p.StdioClients(userCtx) {
// Use background context so the subprocess lives for the CLI
// process lifetime (not killed by a short timeout).
if err := sc.Client.Start(context.Background()); err != nil {
slog.Warn("plugin: failed to start stdio server",
"plugin", p.Manifest.Name, "server", sc.Key, "error", err)
continue
}
stdioEntries = append(stdioEntries, stdioEntry{plugin: p, sc: sc})
registerStdioServerFromManifest(p, sc)
}
}
coldTimeouts := resolvePluginColdTimeouts()
// Phase A: stdio overlay-first registration (synchronous, no I/O).
// Plugins whose overlay.json declares ToolOverrides register their
// server descriptor up-front from manifest metadata alone.
var legacyStdioEntries []stdioEntry
for _, e := range stdioEntries {
_, _, ok := registerStdioServerFromOverlay(e.plugin, e.sc, runner)
if !ok {
legacyStdioEntries = append(legacyStdioEntries, e)
continue
}
}
// Phase B: fan out discovery in parallel.
httpResults := make([][]*cobra.Command, len(httpServers))
legacyStdioResults := make([][]*cobra.Command, len(legacyStdioEntries))
var wg sync.WaitGroup
for i, ps := range httpServers {
wg.Add(1)
go func(idx int, ps pluginServer) {
defer wg.Done()
httpResults[idx] = registerHTTPServer(ps.plugin, ps.srv, tc, runner, coldTimeouts)
}(i, ps)
}
// legacy stdio: discovery-first (commands depend on tool list).
for i, e := range legacyStdioEntries {
wg.Add(1)
go func(idx int, e stdioEntry) {
defer wg.Done()
legacyStdioResults[idx] = registerStdioServer(e.plugin, e.sc, runner, coldTimeouts)
}(i, e)
}
wg.Wait()
for _, cmds := range httpResults {
pluginCmds = append(pluginCmds, cmds...)
}
for _, cmds := range legacyStdioResults {
pluginCmds = append(pluginCmds, cmds...)
}
// 5. Register plugin hooks into pipeline engine
if engine != nil {
for _, p := range allPlugins {
@@ -951,89 +841,14 @@ func loadPlugins(engine *pipeline.Engine, runner executor.Runner) []*cobra.Comma
)
}
return pluginCmds
}
// registerHTTPServer discovers tools from a streamable-http MCP server and
// registers the server. Dynamic command building has been removed; this now
// simply registers the server descriptor for direct runtime dispatch.
func registerHTTPServer(p *plugin.Plugin, srv mcptypes.ServerDescriptor, tc *transport.Client, runner executor.Runner, timeouts pluginColdTimeouts) []*cobra.Command {
tools := discoverHTTPTools(p, srv, tc, timeouts)
return buildHTTPCommandsFromTools(srv, tools, runner)
}
// discoverHTTPTools performs the blocking Initialize + ListTools handshake
// for an HTTP MCP server and returns the discovered tools. Returns nil on
// any transport/protocol error; errors are logged at Debug level.
func discoverHTTPTools(p *plugin.Plugin, srv mcptypes.ServerDescriptor, tc *transport.Client, timeouts pluginColdTimeouts) []transport.ToolDescriptor {
// Cold-path budget. An unreachable endpoint will burn the full window
// via the TCP dial timeout; a healthy localhost/third-party endpoint
// typically responds in <200 ms. Third-party servers with auth get a
// slightly larger window to accommodate TLS + auth RTT. Operators with
// cross-region endpoints can relax the window via DWS_PLUGIN_COLD_TIMEOUT.
// TODO(remove-discovery): plugin discovery currently has no warm cache, so
// unreachable endpoints still pay this timeout during command startup.
timeout := timeouts.httpNoAuth
if len(srv.AuthHeaders) > 0 {
timeout = timeouts.httpAuth
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
discoveryClient := tc
if len(srv.AuthHeaders) > 0 {
discoveryClient = buildPluginAuthClient(tc, srv)
}
if _, err := discoveryClient.Initialize(ctx, srv.Endpoint); err != nil {
slog.Debug("plugin: http server offline, skipping tool discovery",
"plugin", p.Manifest.Name, "server", srv.Key)
return nil
}
toolsResult, err := discoveryClient.ListTools(ctx, srv.Endpoint)
if err != nil {
slog.Debug("plugin: http ListTools failed",
"plugin", p.Manifest.Name, "server", srv.Key, "error", err)
return nil
}
return toolsResult.Tools
}
// buildHTTPCommandsFromTools registers the server for direct runtime
// dispatch. Dynamic command tree building has been removed.
func buildHTTPCommandsFromTools(srv mcptypes.ServerDescriptor, tools []transport.ToolDescriptor, runner executor.Runner) []*cobra.Command {
_ = srv
_ = tools
_ = runner
// Dynamic command building from compat.BuildDynamicCommands has been removed.
return nil
}
// buildPluginAuthClient creates a transport.Client copy with the plugin's
// Bearer token and trusted domains injected. This allows third-party MCP
// servers that require independent authentication to be discovered at startup.
func buildPluginAuthClient(base *transport.Client, srv mcptypes.ServerDescriptor) *transport.Client {
authToken := ""
extraHeaders := make(map[string]string)
for key, value := range srv.AuthHeaders {
if strings.EqualFold(key, "Authorization") {
authToken = strings.TrimPrefix(value, "Bearer ")
authToken = strings.TrimSpace(authToken)
} else {
extraHeaders[key] = value
}
func registerPluginHTTPServer(srv mcptypes.ServerDescriptor) {
AppendDynamicServer(srv)
if len(srv.AuthHeaders) > 0 {
registerPluginAuthFromHeaders(srv)
}
if authToken == "" {
return base
}
client := base.WithAuth(authToken, extraHeaders)
// Trust the endpoint's hostname so the token is actually sent.
if parsed, err := url.Parse(srv.Endpoint); err == nil {
host := parsed.Hostname()
client.TrustedDomains = []string{host, "*." + host}
}
return client
}
// registerPluginAuthFromHeaders extracts authentication credentials from
@@ -1070,84 +885,6 @@ func registerPluginAuthFromHeaders(srv mcptypes.ServerDescriptor) {
})
}
// registerStdioServer initializes a stdio MCP server, discovers its tools,
// and registers the StdioClient for runtime dispatch.
func registerStdioServer(p *plugin.Plugin, sc plugin.StdioServerClient, runner executor.Runner, timeouts pluginColdTimeouts) []*cobra.Command {
tools := discoverStdioTools(p, sc, timeouts)
return buildStdioCommands(p, sc, tools, runner)
}
// discoverStdioTools performs the blocking Initialize + ListTools handshake
// on a stdio MCP subprocess. Returns nil on any error (logged at Debug level).
// The default 2s budget comfortably accommodates Python/Node runtimes whose
// interpreter + dependency load dominates the first response. Operators with
// heavier startup chains can relax further via DWS_PLUGIN_COLD_TIMEOUT.
//
// A handshake failure here is an EXPECTED, benign outcome for an optional local
// plugin: e.g. the conference plugin reports "本地服务未就绪" whenever the
// DingTalk desktop client isn't running, which is the common case for anyone
// not actively recording a meeting. Discovery simply yields no tools and the
// run proceeds — commands that ship toolOverrides still register up-front via
// registerStdioServerFromOverlay (Phase A), so availability is unaffected.
//
// These run during command-tree construction (NewRootCommandWithEngine), which
// happens BEFORE PersistentPreRunE applies --debug/--verbose via
// configureLogLevel. So a Warn here printed to stderr on EVERY invocation
// regardless of flags, polluting output and misleading callers into treating it
// as the cause of an unrelated command error (e.g. an auth or PARAM_ERROR from a
// completely different server). Logging at Debug keeps the discovery miss out of
// normal output; surfacing it would require configuring the log level before the
// tree is built, which we deliberately avoid this close to release.
func discoverStdioTools(p *plugin.Plugin, sc plugin.StdioServerClient, timeouts pluginColdTimeouts) []transport.ToolDescriptor {
ctx, cancel := context.WithTimeout(context.Background(), timeouts.stdio)
defer cancel()
if _, err := sc.Client.Initialize(ctx); err != nil {
slog.Debug("plugin: stdio initialize failed",
"plugin", p.Manifest.Name, "server", sc.Key, "error", err)
return nil
}
toolsResult, err := sc.Client.ListTools(ctx)
if err != nil {
slog.Debug("plugin: stdio ListTools failed",
"plugin", p.Manifest.Name, "server", sc.Key, "error", err)
return nil
}
return toolsResult.Tools
}
// buildStdioCommands registers the stdio client and server descriptor
// for direct runtime dispatch. Dynamic command tree building has been removed.
func buildStdioCommands(p *plugin.Plugin, sc plugin.StdioServerClient, tools []transport.ToolDescriptor, runner executor.Runner) []*cobra.Command {
if len(tools) == 0 {
slog.Debug("plugin: stdio server has no tools",
"plugin", p.Manifest.Name, "server", sc.Key)
return nil
}
overlay := resolveStdioOverlay(p, sc)
descriptor := mcptypes.ServerDescriptor{
Key: sc.Key,
DisplayName: p.Manifest.Name + "/" + sc.Key,
Description: p.Manifest.Description,
Endpoint: StdioEndpoint(p.Manifest.Name, sc.Key),
Source: "plugin",
CLI: overlay,
HasCLIMeta: true,
}
AppendDynamicServer(descriptor)
RegisterStdioClient(p.Manifest.Name+"/"+sc.Key, sc.Client)
slog.Debug("plugin: stdio server registered",
"plugin", p.Manifest.Name, "server", sc.Key,
"tools", len(tools))
_ = runner
return nil
}
// newPipelineEngine creates and configures the pipeline engine with
// handlers for all five pipeline phases. The phases execute in order:
// Register → PreParse → PostParse → PreRequest → PostResponse.
+23
View File
@@ -27,6 +27,7 @@ import (
"sync"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit"
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
@@ -142,6 +143,7 @@ func newCommandRunnerWithFlags(loader cli.CatalogLoader, flags *GlobalFlags) exe
scanner: newRuntimeContentScanner(),
enforceContentScan: runtimeFlagEnabled(os.Getenv(runtimeContentScanEnforceEnv), false),
includeScanReport: runtimeFlagEnabled(os.Getenv(runtimeContentScanReportOutputEnv), false),
auditSink: setupAuditSink(),
}
}
@@ -153,9 +155,22 @@ type runtimeRunner struct {
scanner safety.Scanner
enforceContentScan bool
includeScanReport bool
auditSink audit.Sink
}
func (r *runtimeRunner) Run(ctx context.Context, invocation executor.Invocation) (executor.Result, error) {
// Global dry-run is an execution barrier, not merely a transport option.
// Return a deterministic local preview before profile resolution, catalog
// discovery, Keychain/token prefetch, auth, stateful preflight or transport.
// Use the non-injectable EchoRunner rather than r.fallback so tests and
// edition overlays cannot accidentally turn this path into real execution.
if invocation.DryRun || (r != nil && r.globalFlags != nil && r.globalFlags.DryRun) {
invocation.DryRun = true
return (executor.EchoRunner{}).Run(ctx, invocation)
}
if r == nil {
return executor.Result{}, fmt.Errorf("runtime runner is not configured")
}
// Emit the one-shot host-owned PAT decision log. Placed here (not in
// the constructor) so it fires AFTER PersistentPreRunE has configured
// slog level per --debug / --verbose. The Once guard makes repeat
@@ -468,6 +483,7 @@ func (r *runtimeRunner) executeInvocation(ctx context.Context, endpoint string,
logging.LogCommandEnd(fl, execID,
invocation.CanonicalProduct, invocation.Tool,
retErr == nil, time.Since(invokeStart), errCat, errReason)
emitAudit(r.auditSink, execID, invokeStart, invocation, endpoint, retErr, version)
}()
// Check if this product has plugin-level auth credentials registered.
@@ -704,6 +720,13 @@ func (r *runtimeRunner) executeStdioInvocation(ctx context.Context, invocation e
callCtx, cancel = context.WithTimeout(ctx, time.Duration(r.globalFlags.Timeout)*time.Second)
defer cancel()
}
if err := client.EnsureInitialized(callCtx); err != nil {
return executor.Result{}, apperrors.NewAPI(
fmt.Sprintf("stdio initialize failed: %v", err),
apperrors.WithOperation("initialize"),
apperrors.WithReason("stdio_initialize_error"),
)
}
callResult, err := client.CallTool(callCtx, invocation.Tool, invocation.Params)
if err != nil {
@@ -0,0 +1,554 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync/atomic"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/fatih/color"
)
var (
manualAgentExamplePlaceholderPattern = regexp.MustCompile(`<([^>]+)>`)
manualAgentExampleDryRunJSONPattern = regexp.MustCompile(`(?i)"dry_run"\s*:\s*true`)
)
// TestManualAgentExamplesContract is the always-on gate. It validates every
// example, including contract_only entries, against the live bound Cobra path,
// flags, required arguments, constraints, and final typed safety.
func TestManualAgentExamplesContract(t *testing.T) {
plan := manualAgentExampleExecutionPlan(t)
if plan.Total == 0 {
t.Fatal("no reviewed Agent examples were contract validated")
}
t.Logf("Agent example contract: total=%d contract=%d dry_run=%d contract_only=%d", plan.Total, plan.Contract, plan.DryRun, plan.ContractOnly)
}
// TestManualAgentExamplesDryRun first validates every reviewed example against
// its real BoundCommand, Cobra required arguments, and final typed constraints.
// It then executes only the deterministic, explicitly declared dry_run subset
// without injecting --yes. Global flag inheritance is not treated as capability
// evidence. Runtime failures never create implicit skips. No shell is involved
// and HOME is isolated.
func TestManualAgentExamplesDryRun(t *testing.T) {
if os.Getenv("DWS_AGENT_EXAMPLES_DRY_RUN") != "1" {
t.Skip("set DWS_AGENT_EXAMPLES_DRY_RUN=1 to execute the explicitly reviewed Agent dry-run subset")
}
sandboxRoot := t.TempDir()
homeDir := filepath.Join(sandboxRoot, "home")
configDir := filepath.Join(sandboxRoot, "config")
for _, dir := range []string{homeDir, configDir} {
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatalf("create isolated test directory %s: %v", dir, err)
}
}
t.Setenv("HOME", homeDir)
t.Setenv("DWS_CONFIG_DIR", configDir)
t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1")
t.Setenv("NO_PROXY", "")
plan := manualAgentExampleExecutionPlan(t)
if plan.Total == 0 {
t.Fatal("no reviewed Agent examples were contract validated")
}
t.Chdir(sandboxRoot)
files := newManualAgentExampleFiles(t, sandboxRoot)
selected := 0
executed := 0
for _, execution := range plan.Examples {
if !manualAgentExampleShouldExerciseDryRun(execution) {
continue
}
selected++
execution := execution
t.Run(fmt.Sprintf("%s/%d", strings.ReplaceAll(execution.CanonicalPath, ".", "/"), execution.Index), func(t *testing.T) {
argv, err := cli.ParseManualAgentExampleArgv(execution.Example)
if err != nil {
t.Fatalf("parse example %q: %v", execution.Example, err)
}
args := materializeManualAgentExampleArgv(argv[1:], files)
if manualAgentExampleHasFlag(args, "yes") {
t.Fatalf("dry-run gate must not inject or accept --yes\nsource: %s\nargv: %q", execution.Example, args)
}
if !manualAgentExampleHasFlag(args, "dry-run") {
args = append([]string{"--dry-run"}, args...)
}
capture, err := executeManualAgentExampleCapture(t, args)
if capture.ToolCallAttempts != 0 {
t.Fatalf("eligible dry-run attempted %d ToolCaller invocation(s)\nsource: %s\nargv: %q\noutput:\n%s", capture.ToolCallAttempts, execution.Example, args, capture.Output)
}
if capture.StdinBytesRead != 0 || manualAgentExamplePromptObserved(capture.Output) {
t.Fatalf("eligible dry-run entered an interactive confirmation path (stdin bytes read: %d)\nsource: %s\nargv: %q\noutput:\n%s", capture.StdinBytesRead, execution.Example, args, capture.Output)
}
if err != nil {
t.Fatalf("dry-run example failed: %v\nsource: %s\nargv: %q\noutput:\n%s", err, execution.Example, args, capture.Output)
}
previewKind, observed := manualAgentExampleDryRunEvidence(capture)
if !observed {
t.Fatalf("example returned without audited dry-run evidence (caller dry-run checks: %d)\nsource: %s\nargv: %q\noutput:\n%s", capture.DryRunChecks, execution.Example, args, capture.Output)
}
if want := execution.DryRun.PreviewKind; previewKind != want {
t.Fatalf("dry-run preview kind = %q, Schema declares %q\nsource: %s\nargv: %q\noutput:\n%s", previewKind, want, execution.Example, args, capture.Output)
}
t.Logf("dry_run_capability_candidate=%s", previewKind)
executed++
})
}
if executed != selected {
t.Fatalf("executed dry_run examples = %d, selected capability set requires %d", executed, selected)
}
t.Logf("Agent examples: total=%d contract=%d dry_run_selected=%d planned_dry_run=%d contract_only=%d reviewed_manual=%d", plan.Total, plan.Contract, selected, plan.DryRun, plan.ContractOnly, plan.ReviewedContractOnly)
reasonCodes := make([]string, 0, len(plan.ContractOnlyByReason))
for reasonCode := range plan.ContractOnlyByReason {
reasonCodes = append(reasonCodes, string(reasonCode))
}
sort.Strings(reasonCodes)
for _, reasonCode := range reasonCodes {
t.Logf("Agent examples contract_only[%s]=%d", reasonCode, plan.ContractOnlyByReason[cli.ManualAgentExampleReasonCode(reasonCode)])
}
}
// manualAgentExampleShouldExerciseDryRun is the single selection boundary for
// the runtime gate. Capability comes only from the final typed ToolSpec; the
// example disposition may narrow that set but can never invent support.
func manualAgentExampleShouldExerciseDryRun(execution cli.ManualAgentExampleExecution) bool {
return execution.DryRun != nil && execution.Mode == cli.ManualAgentExampleModeDryRun
}
func manualAgentExampleExecutionPlan(t testing.TB) cli.ManualAgentExampleExecutionPlan {
t.Helper()
hints, err := cli.LoadAgentHintsFromSelectionForValidation(os.DirFS("../cli/schema_hints/selection"))
if err != nil {
t.Fatalf("LoadAgentHintsFromSelectionForValidation() error = %v", err)
}
contractRoot := NewRootCommand()
if _, err := cli.ApplyEmbeddedManualSchemaHints(contractRoot); err != nil {
t.Fatalf("ApplyEmbeddedManualSchemaHints() error = %v", err)
}
effective, err := cli.BuildEffectiveCommandRegistry(contractRoot)
if err != nil {
t.Fatalf("BuildEffectiveCommandRegistry() error = %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(contractRoot, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
registry, err := cli.AssembleSchemaRegistryFromBound(bound)
if err != nil {
t.Fatalf("AssembleSchemaRegistryFromBound() error = %v", err)
}
if err := cli.ValidateReviewedDryRunCapabilityDelivery(registry); err != nil {
t.Fatalf("ValidateReviewedDryRunCapabilityDelivery() error = %v", err)
}
plan, err := cli.BuildManualAgentExampleExecutionPlan(bound, registry, hints)
if err != nil {
t.Fatalf("BuildManualAgentExampleExecutionPlan() error = %v", err)
}
return plan
}
type manualAgentExampleCapture struct {
Output string
DryRunChecks int64
ToolCallAttempts int64
StdinBytesRead int64
}
type manualAgentExampleFailClosedCaller struct {
dryRunChecks atomic.Int64
toolCallAttempts atomic.Int64
}
func (c *manualAgentExampleFailClosedCaller) CallTool(_ context.Context, productID, toolName string, _ map[string]any) (*edition.ToolResult, error) {
c.toolCallAttempts.Add(1)
return nil, fmt.Errorf("real ToolCaller invocation blocked during Agent example dry-run: %s/%s", productID, toolName)
}
func (c *manualAgentExampleFailClosedCaller) Format() string { return "json" }
func (c *manualAgentExampleFailClosedCaller) DryRun() bool {
c.dryRunChecks.Add(1)
return true
}
func (c *manualAgentExampleFailClosedCaller) Fields() string { return "" }
func (c *manualAgentExampleFailClosedCaller) JQ() string { return "" }
func executeManualAgentExampleCapture(t testing.TB, args []string) (manualAgentExampleCapture, error) {
t.Helper()
oldArgs := os.Args
os.Args = append([]string{"dws"}, args...)
defer func() { os.Args = oldArgs }()
oldStdin := os.Stdin
promptInput, err := os.CreateTemp(t.TempDir(), "agent-example-stdin-*.txt")
if err != nil {
t.Fatalf("open guarded stdin: %v", err)
}
defer promptInput.Close()
if _, err := promptInput.WriteString("no\n"); err != nil {
t.Fatalf("seed guarded stdin: %v", err)
}
if _, err := promptInput.Seek(0, io.SeekStart); err != nil {
t.Fatalf("rewind guarded stdin: %v", err)
}
os.Stdin = promptInput
defer func() { os.Stdin = oldStdin }()
oldStdout, oldStderr := os.Stdout, os.Stderr
oldColorOutput, oldColorError := color.Output, color.Error
captureFile, err := os.CreateTemp(t.TempDir(), "agent-example-output-*.log")
if err != nil {
t.Fatalf("open output capture file: %v", err)
}
defer captureFile.Close()
os.Stdout, os.Stderr = captureFile, captureFile
color.Output, color.Error = captureFile, captureFile
defer func() {
os.Stdout, os.Stderr = oldStdout, oldStderr
color.Output, color.Error = oldColorOutput, oldColorError
}()
root := NewRootCommand()
originalCaller := helpers.GetCaller()
auditCaller := &manualAgentExampleFailClosedCaller{}
helpers.InitDeps(auditCaller)
defer helpers.InitDeps(originalCaller)
var output bytes.Buffer
root.SetOut(&output)
root.SetErr(&output)
root.SetArgs(args)
execErr := root.Execute()
os.Stdout, os.Stderr = oldStdout, oldStderr
color.Output, color.Error = oldColorOutput, oldColorError
if _, err := captureFile.Seek(0, io.SeekStart); err != nil {
t.Fatalf("rewind output capture file: %v", err)
}
captured, readErr := io.ReadAll(captureFile)
if readErr != nil {
t.Fatalf("read output capture file: %v", readErr)
}
stdinBytesRead, err := promptInput.Seek(0, io.SeekCurrent)
if err != nil {
t.Fatalf("inspect guarded stdin: %v", err)
}
return manualAgentExampleCapture{
Output: output.String() + string(captured),
DryRunChecks: auditCaller.dryRunChecks.Load(),
ToolCallAttempts: auditCaller.toolCallAttempts.Load(),
StdinBytesRead: stdinBytesRead,
}, execErr
}
type manualAgentExampleFiles struct {
root string
markdown string
json string
batch string
binary string
image string
}
func newManualAgentExampleFiles(t testing.TB, root string) manualAgentExampleFiles {
t.Helper()
markdown := filepath.Join(root, "content.md")
jsonFile := filepath.Join(root, "report.json")
batch := filepath.Join(root, "styles.json")
binary := filepath.Join(root, "report.pdf")
image := filepath.Join(root, "chart.png")
for path, content := range map[string][]byte{
markdown: []byte("# Agent dry-run fixture\n\nNo business call is allowed.\n"),
jsonFile: []byte(`[{"content":"Agent dry-run fixture","sort":"0","key":"fixture","contentType":"markdown","type":"1"}]`),
batch: []byte(`[{"sheetId":"Sheet1","range":"A1:B2","fontWeight":"bold"}]`),
binary: []byte("%PDF-1.4\n%%EOF\n"),
image: {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'},
} {
if err := os.WriteFile(path, content, 0o600); err != nil {
t.Fatalf("write dry-run fixture %s: %v", path, err)
}
}
return manualAgentExampleFiles{root: root, markdown: markdown, json: jsonFile, batch: batch, binary: binary, image: image}
}
func materializeManualAgentExampleArgv(argv []string, files manualAgentExampleFiles) []string {
result := append([]string(nil), argv...)
for index := range result {
result[index] = manualAgentExamplePlaceholderPattern.ReplaceAllStringFunc(result[index], func(match string) string {
name := strings.TrimSuffix(strings.TrimPrefix(match, "<"), ">")
switch strings.ToLower(name) {
case "basetime", "remindertimestamp", "reminder-time-stamp":
return "1780000000000"
case "duedateoffset", "due-date-offset":
return "0"
case "reminderrules", "reminder-rules":
return `[{"remindType":"minute","remindTime":10}]`
case "filepath", "file-path":
return files.binary
case "uuid1,uuid2":
return "uuid1,uuid2"
default:
clean := strings.NewReplacer(",", "_", "-", "_", ".", "_").Replace(name)
return "test_" + clean
}
})
}
for index := 0; index < len(result); index++ {
name, inline, ok := manualAgentExampleLongFlag(result[index])
if !ok {
continue
}
valueIndex := index + 1
value := inline
if inline == "" && valueIndex < len(result) {
value = result[valueIndex]
}
replacement := ""
switch name {
case "file", "file-path":
if strings.Contains(strings.ToLower(value), "png") {
replacement = files.image
} else {
replacement = files.binary
}
case "content-file":
replacement = files.markdown
case "contents-file":
replacement = files.json
case "batch":
if strings.HasSuffix(strings.ToLower(value), "styles.json") {
replacement = files.batch
}
case "output":
if value == "." || value == "" {
replacement = files.root
} else {
replacement = filepath.Join(files.root, filepath.Base(value))
}
}
if replacement == "" {
continue
}
if inline != "" {
result[index] = "--" + name + "=" + replacement
} else if valueIndex < len(result) {
result[valueIndex] = replacement
index++
}
}
return result
}
func manualAgentExampleLongFlag(argument string) (name, inline string, ok bool) {
if !strings.HasPrefix(argument, "--") {
return "", "", false
}
name, inline, _ = strings.Cut(strings.TrimPrefix(argument, "--"), "=")
return name, inline, name != ""
}
func manualAgentExampleHasFlag(argv []string, target string) bool {
for _, argument := range argv {
if argument == "--"+target || strings.HasPrefix(argument, "--"+target+"=") {
return true
}
}
return false
}
func manualAgentExampleDryRunObserved(capture manualAgentExampleCapture) bool {
_, ok := manualAgentExampleDryRunEvidence(capture)
return ok
}
func manualAgentExampleDryRunEvidence(capture manualAgentExampleCapture) (string, bool) {
normalized := strings.ToLower(capture.Output)
if manualAgentExampleDryRunJSONPattern.MatchString(capture.Output) {
return cli.DryRunPreviewRequest, true
}
if strings.Contains(normalized, "[dry-run]") {
return cli.DryRunPreviewInvocation, true
}
if capture.DryRunChecks > 0 && strings.Contains(capture.Output, "操作:") {
return cli.DryRunPreviewPlan, true
}
return "", false
}
func TestManualAgentExampleDryRunEvidenceAcceptsSharedAndCommandPlans(t *testing.T) {
if !manualAgentExampleDryRunObserved(manualAgentExampleCapture{Output: "[DRY-RUN] Preview only, not executed:\nTool: calendar_list"}) {
t.Fatal("dry-run output with a Tool and nil Arguments was not recognized")
}
if manualAgentExampleDryRunObserved(manualAgentExampleCapture{Output: "Tool: calendar_list"}) {
t.Fatal("a Tool line without dry-run evidence must not be accepted")
}
for _, falseEvidence := range []string{
"unknown flag: --dry-run",
"Run again with --dry-run to preview the operation",
`{"dry_run":false,"executed":true}`,
} {
if manualAgentExampleDryRunObserved(manualAgentExampleCapture{Output: falseEvidence}) {
t.Errorf("non-evidence text was mistaken for a successful dry-run: %q", falseEvidence)
}
}
operationSummary := "操作: 下载钉盘文件\n文件ID: test"
if manualAgentExampleDryRunObserved(manualAgentExampleCapture{Output: operationSummary}) {
t.Fatal("a human-only operation summary without an audited dry-run check must not be accepted")
}
if !manualAgentExampleDryRunObserved(manualAgentExampleCapture{Output: operationSummary, DryRunChecks: 1}) {
t.Fatal("a command plan guarded by the injected caller's dry-run check was not recognized")
}
}
func manualAgentExamplePromptObserved(output string) bool {
normalized := strings.ToLower(output)
for _, marker := range []string{
"confirm ",
"confirm deletion?",
"confirm action?",
"confirm create?",
"confirm update?",
"confirm save?",
"confirm import?",
"are you sure",
"operation cancelled",
"操作已取消",
} {
if strings.Contains(normalized, marker) {
return true
}
}
return false
}
func TestManualAgentExamplePromptObservedRejectsInteractiveConfirmation(t *testing.T) {
for _, prompt := range []string{
"Confirm deletion? (yes/no):",
"Confirm action? (yes/no):",
"Confirm create? (yes/no):",
"Confirm update? (yes/no):",
"Confirm save? (yes/no):",
"Confirm import? (yes/no):",
"Are you sure you want to continue?",
"Operation cancelled",
} {
if !manualAgentExamplePromptObserved(prompt) {
t.Errorf("interactive confirmation output was not detected: %q", prompt)
}
}
if manualAgentExamplePromptObserved(`{"dry_run":true,"confirmation":"user_required"}`) {
t.Fatal("typed safety metadata was mistaken for an interactive prompt")
}
}
func TestAitableAdvpermDisableDryRunSkipsConfirmationAndToolCall(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
args := []string{
"--dry-run", "--format", "json",
"aitable", "advperm", "disable",
"--base-id", "BASE_ID",
}
if manualAgentExampleHasFlag(args, "yes") {
t.Fatal("regression test must not bypass confirmation with --yes")
}
capture, err := executeManualAgentExampleCapture(t, args)
if err != nil {
t.Fatalf("advperm disable fail-closed dry-run failed: %v\noutput:\n%s", err, capture.Output)
}
if capture.StdinBytesRead != 0 || manualAgentExamplePromptObserved(capture.Output) {
t.Fatalf("advperm disable dry-run entered confirmation (stdin bytes read: %d)\noutput:\n%s", capture.StdinBytesRead, capture.Output)
}
if capture.ToolCallAttempts != 0 {
t.Fatalf("advperm disable dry-run attempted %d real ToolCaller invocation(s)\noutput:\n%s", capture.ToolCallAttempts, capture.Output)
}
if !manualAgentExampleDryRunObserved(capture) {
t.Fatalf("advperm disable returned no audited dry-run evidence (caller dry-run checks: %d)\noutput:\n%s", capture.DryRunChecks, capture.Output)
}
}
func TestManualAgentExampleFailClosedCallerRecordsToolCalls(t *testing.T) {
caller := &manualAgentExampleFailClosedCaller{}
if !caller.DryRun() {
t.Fatal("fail-closed caller must advertise dry-run mode")
}
if _, err := caller.CallTool(context.Background(), "calendar", "list_events", nil); err == nil {
t.Fatal("fail-closed caller accepted a ToolCaller invocation")
}
if got := caller.dryRunChecks.Load(); got != 1 {
t.Fatalf("DryRun() checks = %d, want 1", got)
}
if got := caller.toolCallAttempts.Load(); got != 1 {
t.Fatalf("CallTool() attempts = %d, want 1", got)
}
}
func TestManualAgentExampleChatGroupMuteMemberUsesCommandDryRunPreview(t *testing.T) {
sandboxRoot := t.TempDir()
configDir := filepath.Join(sandboxRoot, "config")
if err := os.MkdirAll(configDir, 0o700); err != nil {
t.Fatalf("create isolated config directory: %v", err)
}
t.Setenv("HOME", sandboxRoot)
t.Setenv("DWS_CONFIG_DIR", configDir)
capture, err := executeManualAgentExampleCapture(t, []string{
"--dry-run",
"chat", "group-mute-member",
"--group", "test_openConversationId",
"--users", "userId1,userId2",
"--mute-time", "3600000",
})
if err != nil {
t.Fatalf("group-mute-member dry-run failed: %v\noutput:\n%s", err, capture.Output)
}
if capture.ToolCallAttempts != 0 {
t.Fatalf("group-mute-member dry-run attempted %d ToolCaller invocation(s)\noutput:\n%s", capture.ToolCallAttempts, capture.Output)
}
if capture.DryRunChecks == 0 {
t.Fatalf("group-mute-member did not enter its audited command dry-run path\noutput:\n%s", capture.Output)
}
if capture.StdinBytesRead != 0 || manualAgentExamplePromptObserved(capture.Output) {
t.Fatalf("group-mute-member dry-run entered an interactive prompt (stdin bytes read: %d)\noutput:\n%s", capture.StdinBytesRead, capture.Output)
}
if !manualAgentExampleDryRunObserved(capture) {
t.Fatalf("group-mute-member returned no audited dry-run evidence\noutput:\n%s", capture.Output)
}
for _, expected := range []string{`"uids"`, `"userId1"`, `"userId2"`} {
if !strings.Contains(capture.Output, expected) {
t.Fatalf("group-mute-member command preview missing %s\noutput:\n%s", expected, capture.Output)
}
}
if strings.Contains(capture.Output, `"openDingTalkIds"`) {
t.Fatalf("group-mute-member dry-run unexpectedly resolved user IDs remotely\noutput:\n%s", capture.Output)
}
}
@@ -0,0 +1,61 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"os"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
func TestManualAgentSelectionScenariosCoverEveryExecutableSchemaTool(t *testing.T) {
root := NewRootCommand()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("BuildEffectiveCommandRegistry() error = %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
hints, err := cli.LoadAgentHintsFromSelectionForValidation(os.DirFS("../cli/schema_hints/selection"))
if err != nil {
t.Fatalf("LoadAgentHintsFromSelectionForValidation() error = %v", err)
}
fixture, report, err := cli.BuildManualAgentSelectionEvalFixture(bound, hints)
if err != nil {
t.Fatalf("BuildManualAgentSelectionEvalFixture() error = %v", err)
}
if report.Tools != len(bound.Commands) {
t.Fatalf("selection tools = %d, bound commands = %d", report.Tools, len(bound.Commands))
}
if report.PositiveAssertions < report.Tools {
t.Fatalf("positive selection coverage = %+v, want at least one assertion per tool", report)
}
if report.NegativeAssertions < report.Tools {
t.Fatalf("negative selection coverage = %+v, want at least one assertion per tool", report)
}
if report.Tools == 0 {
t.Fatal("selection contract unexpectedly contains no tools")
}
if len(fixture.Cases) != report.PositiveAssertions+report.NegativeAssertions {
t.Fatalf("selection fixture cases = %d, report = %+v", len(fixture.Cases), report)
}
if report.FixtureSHA256 == "" {
t.Fatal("selection fixture digest is empty")
}
t.Logf("validated %d bound tools, %d positive assertions, %d negative assertions (%s)", report.Tools, report.PositiveAssertions, report.NegativeAssertions, report.FixtureSHA256)
}
@@ -0,0 +1,465 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"sort"
"strings"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
const manualAgentSelectionLiveBatchSize = 12
type manualAgentSelectionLiveCandidate struct {
CanonicalPath string `json:"canonical_path"`
AgentSummary string `json:"agent_summary"`
UseWhen []string `json:"use_when"`
AvoidWhen []string `json:"avoid_when"`
}
type manualAgentSelectionLiveInput struct {
Cases []manualAgentSelectionLiveCase `json:"cases"`
Candidates []manualAgentSelectionLiveCandidate `json:"candidates"`
}
// manualAgentSelectionLiveCase is deliberately answer-free. Expected and
// forbidden canonicals stay only in the local assertion fixture and are never
// sent to the model being evaluated.
type manualAgentSelectionLiveCase struct {
ID string `json:"id"`
Scenario string `json:"scenario"`
}
type manualAgentSelectionLiveResult struct {
ID string `json:"id"`
CanonicalPath string `json:"canonical_path"`
}
type manualAgentSelectionLiveResponse struct {
Results []manualAgentSelectionLiveResult `json:"results"`
}
// TestManualAgentSelectionArkLive is intentionally opt-in. Deterministic CI
// validates all fixture and Cobra facts without network access; this test asks
// a real model to interpret the reviewed natural-language scenarios. Set
// DWS_AGENT_SELECTION_FULL=1 to evaluate every positive and negative case.
func TestManualAgentSelectionArkLive(t *testing.T) {
if os.Getenv("DWS_AGENT_SELECTION_LIVE") != "1" {
t.Skip("set DWS_AGENT_SELECTION_LIVE=1 and ARK_API_KEY/ARK_BASE_URL/ARK_MODEL to run live Agent command-selection evaluation")
}
apiKey := strings.TrimSpace(os.Getenv("ARK_API_KEY"))
baseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("ARK_BASE_URL")), "/")
model := strings.TrimSpace(os.Getenv("ARK_MODEL"))
for name, value := range map[string]string{
"ARK_API_KEY": apiKey,
"ARK_BASE_URL": baseURL,
"ARK_MODEL": model,
} {
if value == "" {
t.Fatalf("%s is required when DWS_AGENT_SELECTION_LIVE=1", name)
}
}
if err := validateManualAgentSelectionLiveBaseURL(baseURL, os.Getenv("DWS_AGENT_SELECTION_ALLOWED_BASE_URLS")); err != nil {
t.Fatal(err)
}
fixture, hints := manualAgentSelectionLiveFixture(t)
cases := selectManualAgentSelectionLiveCases(t, fixture.Cases)
for _, batch := range batchManualAgentSelectionLiveCases(cases, manualAgentSelectionLiveBatchSize) {
productID := batch[0].ProductID
t.Run(productID+"/"+sanitizeManualAgentSelectionLiveTestID(batch[0].ID), func(t *testing.T) {
input := buildManualAgentSelectionLiveInput(batch, hints)
results := callManualAgentSelectionLiveModel(t, baseURL, apiKey, model, input)
assertManualAgentSelectionLiveResults(t, batch, results)
})
}
}
func buildManualAgentSelectionLiveInput(batch []cli.ManualAgentSelectionCase, hints cli.ManualAgentHintSet) manualAgentSelectionLiveInput {
input := manualAgentSelectionLiveInput{Cases: make([]manualAgentSelectionLiveCase, 0, len(batch))}
if len(batch) == 0 {
return input
}
for _, selectionCase := range batch {
input.Cases = append(input.Cases, manualAgentSelectionLiveCase{
ID: selectionCase.ID,
Scenario: selectionCase.Scenario,
})
}
input.Candidates = make([]manualAgentSelectionLiveCandidate, 0, len(batch[0].CandidateCanonicals))
for _, canonical := range batch[0].CandidateCanonicals {
hint := hints.Tools[canonical]
input.Candidates = append(input.Candidates, manualAgentSelectionLiveCandidate{
CanonicalPath: canonical,
AgentSummary: hint.AgentSummary,
UseWhen: hint.UseWhen,
AvoidWhen: hint.AvoidWhen,
})
}
return input
}
func manualAgentSelectionLiveFixture(t testing.TB) (cli.ManualAgentSelectionFixture, cli.ManualAgentHintSet) {
t.Helper()
root := NewRootCommand()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("BuildEffectiveCommandRegistry() error = %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
hints, err := cli.LoadAgentHintsFromSelectionForValidation(os.DirFS("../cli/schema_hints/selection"))
if err != nil {
t.Fatalf("LoadAgentHintsFromSelectionForValidation() error = %v", err)
}
fixture, _, err := cli.BuildManualAgentSelectionEvalFixture(bound, hints)
if err != nil {
t.Fatalf("BuildManualAgentSelectionEvalFixture() error = %v", err)
}
return fixture, hints
}
func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.ManualAgentSelectionCase) []cli.ManualAgentSelectionCase {
t.Helper()
if raw := strings.TrimSpace(os.Getenv("DWS_AGENT_SELECTION_CASES")); raw != "" {
selected := map[string]bool{}
for _, id := range strings.Split(raw, ",") {
if id = strings.TrimSpace(id); id != "" {
selected[id] = true
}
}
result := make([]cli.ManualAgentSelectionCase, 0, len(selected))
for _, selectionCase := range cases {
if selected[selectionCase.ID] {
result = append(result, selectionCase)
delete(selected, selectionCase.ID)
}
}
if len(selected) != 0 {
missing := make([]string, 0, len(selected))
for id := range selected {
missing = append(missing, id)
}
sort.Strings(missing)
t.Fatalf("DWS_AGENT_SELECTION_CASES contains unknown case IDs: %s", strings.Join(missing, ", "))
}
return result
}
if os.Getenv("DWS_AGENT_SELECTION_FULL") == "1" {
return append([]cli.ManualAgentSelectionCase(nil), cases...)
}
// Smoke mode exercises one positive and one negative scenario per product.
seenPositive := map[string]bool{}
seenNegative := map[string]bool{}
result := make([]cli.ManualAgentSelectionCase, 0)
for _, selectionCase := range cases {
if selectionCase.ExpectedCanonical != "" && !seenPositive[selectionCase.ProductID] {
seenPositive[selectionCase.ProductID] = true
result = append(result, selectionCase)
}
if selectionCase.ForbiddenCanonical != "" && !seenNegative[selectionCase.ProductID] {
seenNegative[selectionCase.ProductID] = true
result = append(result, selectionCase)
}
}
return result
}
func batchManualAgentSelectionLiveCases(cases []cli.ManualAgentSelectionCase, batchSize int) [][]cli.ManualAgentSelectionCase {
if batchSize <= 0 {
batchSize = 1
}
grouped := map[string][]cli.ManualAgentSelectionCase{}
products := make([]string, 0)
for _, selectionCase := range cases {
if _, ok := grouped[selectionCase.ProductID]; !ok {
products = append(products, selectionCase.ProductID)
}
grouped[selectionCase.ProductID] = append(grouped[selectionCase.ProductID], selectionCase)
}
sort.Strings(products)
result := make([][]cli.ManualAgentSelectionCase, 0)
for _, productID := range products {
productCases := grouped[productID]
for start := 0; start < len(productCases); start += batchSize {
end := start + batchSize
if end > len(productCases) {
end = len(productCases)
}
result = append(result, append([]cli.ManualAgentSelectionCase(nil), productCases[start:end]...))
}
}
return result
}
func callManualAgentSelectionLiveModel(t testing.TB, baseURL, apiKey, model string, input manualAgentSelectionLiveInput) []manualAgentSelectionLiveResult {
t.Helper()
body, err := marshalManualAgentSelectionLiveRequest(baseURL, model, input)
if err != nil {
t.Fatalf("marshal live selection request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/chat/completions", bytes.NewReader(body))
if err != nil {
t.Fatalf("build live selection request: %v", err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
request.Header.Set("Content-Type", "application/json")
client := &http.Client{CheckRedirect: func(request *http.Request, via []*http.Request) error {
if len(via) > 0 && (request.URL.Scheme != via[0].URL.Scheme || !strings.EqualFold(request.URL.Host, via[0].URL.Host)) {
return http.ErrUseLastResponse
}
return nil
}}
response, err := client.Do(request)
if err != nil {
t.Fatalf("live selection model request: %v", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
detail, _ := io.ReadAll(io.LimitReader(response.Body, 2048))
t.Fatalf("live selection model status %s: %s", response.Status, strings.TrimSpace(string(detail)))
}
var envelope struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&envelope); err != nil {
t.Fatalf("decode live selection response envelope: %v", err)
}
if len(envelope.Choices) == 0 || strings.TrimSpace(envelope.Choices[0].Message.Content) == "" {
t.Fatal("live selection response has no model content")
}
var selection manualAgentSelectionLiveResponse
decoder := json.NewDecoder(strings.NewReader(envelope.Choices[0].Message.Content))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&selection); err != nil {
t.Fatalf("decode live selection model JSON: %v; content=%s", err, envelope.Choices[0].Message.Content)
}
return selection.Results
}
func marshalManualAgentSelectionLiveRequest(baseURL, model string, input manualAgentSelectionLiveInput) ([]byte, error) {
inputJSON, err := json.Marshal(input)
if err != nil {
return nil, fmt.Errorf("marshal live selection input: %w", err)
}
requestBody := map[string]any{
"model": model,
"temperature": 0,
"max_tokens": 4096,
"messages": []map[string]string{
{
"role": "system",
"content": "You evaluate DWS Agent command selection. For each case, interpret the natural-language scenario and choose exactly one canonical_path from candidates, or the literal string none when no candidate is appropriate. Return only JSON as {\"results\":[{\"id\":\"case id\",\"canonical_path\":\"candidate or none\"}]}. Return every case ID exactly once. Do not execute commands.",
},
{"role": "user", "content": string(inputJSON)},
},
}
// Ark plan endpoints do not consistently accept response_format. Other
// OpenAI-compatible endpoints get the stricter JSON-object request.
if !strings.HasSuffix(strings.TrimRight(baseURL, "/"), "/api/plan/v3") {
requestBody["response_format"] = map[string]string{"type": "json_object"}
}
return json.Marshal(requestBody)
}
func assertManualAgentSelectionLiveResults(t testing.TB, cases []cli.ManualAgentSelectionCase, results []manualAgentSelectionLiveResult) {
t.Helper()
byID := make(map[string]manualAgentSelectionLiveResult, len(results))
for _, result := range results {
if _, exists := byID[result.ID]; exists {
t.Fatalf("live selection returned duplicate case ID %q", result.ID)
}
byID[result.ID] = result
}
for _, selectionCase := range cases {
result, ok := byID[selectionCase.ID]
if !ok {
t.Errorf("live selection omitted case %q", selectionCase.ID)
continue
}
delete(byID, selectionCase.ID)
selected := strings.TrimSpace(result.CanonicalPath)
if selected == "" {
t.Errorf("live selection returned empty canonical for %q", selectionCase.ID)
continue
}
if selected != "none" && !containsManualAgentSelectionCanonical(selectionCase.CandidateCanonicals, selected) {
t.Errorf("live selection returned non-candidate %q for %q", selected, selectionCase.ID)
continue
}
if selectionCase.ExpectedCanonical != "" && selected != selectionCase.ExpectedCanonical {
t.Errorf("live positive selection %q = %q, want %q; scenario=%q", selectionCase.ID, selected, selectionCase.ExpectedCanonical, selectionCase.Scenario)
}
if selectionCase.ForbiddenCanonical != "" && selected == selectionCase.ForbiddenCanonical {
t.Errorf("live negative selection %q chose forbidden %q; scenario=%q", selectionCase.ID, selected, selectionCase.Scenario)
}
}
if len(byID) != 0 {
unexpected := make([]string, 0, len(byID))
for id := range byID {
unexpected = append(unexpected, id)
}
sort.Strings(unexpected)
t.Errorf("live selection returned unexpected case IDs: %s", strings.Join(unexpected, ", "))
}
}
func validateManualAgentSelectionLiveBaseURL(raw, extraAllowed string) error {
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.User != nil {
return fmt.Errorf("ARK_BASE_URL must be an absolute HTTP(S) API base without query or fragment")
}
if parsed.Scheme == "http" {
if !manualAgentSelectionLoopbackHost(parsed.Hostname()) {
return fmt.Errorf("ARK_BASE_URL may use plaintext HTTP only for a loopback test endpoint")
}
return nil
}
if parsed.Scheme != "https" {
return fmt.Errorf("ARK_BASE_URL must use HTTPS, except for a loopback HTTP test endpoint")
}
allowed := map[string]bool{
"https://ark.ap-southeast.bytepluses.com/api/v3": true,
"https://ark.cn-beijing.volces.com/api/plan/v3": true,
}
for _, candidate := range strings.Split(extraAllowed, ",") {
candidate = strings.TrimRight(strings.TrimSpace(candidate), "/")
if candidate != "" {
allowed[candidate] = true
}
}
normalized := strings.TrimRight(raw, "/")
if !allowed[normalized] {
return fmt.Errorf("ARK_BASE_URL %q is not allowlisted; use a built-in Ark base or add the exact HTTPS base to DWS_AGENT_SELECTION_ALLOWED_BASE_URLS", raw)
}
return nil
}
func manualAgentSelectionLoopbackHost(host string) bool {
if strings.EqualFold(strings.TrimSpace(host), "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func containsManualAgentSelectionCanonical(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func sanitizeManualAgentSelectionLiveTestID(value string) string {
value = strings.ReplaceAll(value, ".", "_")
value = strings.ReplaceAll(value, "/", "_")
return value
}
func TestManualAgentSelectionLiveResultContract(t *testing.T) {
cases := []cli.ManualAgentSelectionCase{
{ID: "sample.search/use_when/0", Scenario: "find an item", ExpectedCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}},
{ID: "sample.search/avoid_when/0", Scenario: "create an item", ForbiddenCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}},
}
t.Run("accepts exact positive and negative choices", func(t *testing.T) {
assertManualAgentSelectionLiveResults(t, cases, []manualAgentSelectionLiveResult{
{ID: cases[0].ID, CanonicalPath: "sample.search"},
{ID: cases[1].ID, CanonicalPath: "sample.create"},
})
})
}
func TestManualAgentSelectionLiveInputDoesNotLeakAssertionsOrRepeatCandidates(t *testing.T) {
batch := []cli.ManualAgentSelectionCase{
{ID: "sample.search/use_when/0", Scenario: "find an item", ExpectedCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}},
{ID: "sample.search/avoid_when/0", Scenario: "create an item", ForbiddenCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}},
}
hints := cli.ManualAgentHintSet{Tools: map[string]cli.ManualAgentToolHint{
"sample.create": {AgentSummary: "Create an item", UseWhen: []string{"create"}, AvoidWhen: []string{"find"}},
"sample.search": {AgentSummary: "Search items", UseWhen: []string{"find"}, AvoidWhen: []string{"create"}},
}}
input := buildManualAgentSelectionLiveInput(batch, hints)
data, err := marshalManualAgentSelectionLiveRequest("https://ark.cn-beijing.volces.com/api/plan/v3", "fixed-model", input)
if err != nil {
t.Fatal(err)
}
for _, forbiddenKey := range []string{"expected_canonical", "forbidden_canonical", "candidate_canonicals"} {
if strings.Contains(string(data), forbiddenKey) {
t.Fatalf("live model payload leaks local assertion field %q: %s", forbiddenKey, data)
}
}
if len(input.Candidates) != 2 || len(input.Cases) != 2 {
t.Fatalf("live model input = %+v", input)
}
if count := strings.Count(string(data), `\"candidates\"`); count != 1 {
t.Fatalf("live request contains candidate table %d times, want once: %s", count, data)
}
}
func TestValidateManualAgentSelectionLiveBaseURL(t *testing.T) {
tests := []struct {
name string
baseURL string
extraAllowed string
wantErr string
}{
{name: "built-in Ark", baseURL: "https://ark.cn-beijing.volces.com/api/plan/v3"},
{name: "loopback localhost", baseURL: "http://localhost:8080/v1"},
{name: "loopback IPv4", baseURL: "http://127.0.0.1:8080/v1"},
{name: "loopback IPv6", baseURL: "http://[::1]:8080/v1"},
{name: "allowlisted HTTPS extension", baseURL: "https://models.example.test/v1", extraAllowed: "https://models.example.test/v1"},
{name: "plaintext remote", baseURL: "http://models.example.test/v1", wantErr: "only for a loopback"},
{name: "HTTPS not allowlisted", baseURL: "https://models.example.test/v1", wantErr: "not allowlisted"},
{name: "allowlist path mismatch", baseURL: "https://models.example.test/v2", extraAllowed: "https://models.example.test/v1", wantErr: "not allowlisted"},
{name: "URL credentials", baseURL: "https://token@ark.cn-beijing.volces.com/api/plan/v3", wantErr: "absolute HTTP(S)"},
{name: "query", baseURL: "https://ark.cn-beijing.volces.com/api/plan/v3?q=1", wantErr: "absolute HTTP(S)"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateManualAgentSelectionLiveBaseURL(test.baseURL, test.extraAllowed)
if test.wantErr == "" {
if err != nil {
t.Fatalf("validate base URL: %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %v, want containing %q", err, test.wantErr)
}
})
}
}
@@ -0,0 +1,283 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"bytes"
"fmt"
"sort"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
)
// TestFinalSchemaToolsHaveExecutableBaseCommands is the final Schema-to-Cobra
// delivery gate. It starts from the reviewed CommandRegistry and live Cobra
// tree, then verifies the complete final Schema projection against the bound
// commands. The Catalog is observed only as a delivery output; it is never
// used to discover or synthesize a command identity.
func TestFinalSchemaToolsHaveExecutableBaseCommands(t *testing.T) {
root := NewRootCommand()
snapshot := fullSchemaSnapshotForTest(t)
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("build EffectiveCommandRegistry: %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("bind EffectiveCommandRegistry to live Cobra tree: %v", err)
}
publicCanonicals := make([]string, 0, len(bound.Commands))
for _, command := range bound.Commands {
if command.Visibility == cli.SchemaVisibilityPublic {
publicCanonicals = append(publicCanonicals, command.CanonicalPath)
}
}
sort.Strings(publicCanonicals)
finalCanonicals := make([]string, 0, len(snapshot.Tools))
for canonical := range snapshot.Tools {
finalCanonicals = append(finalCanonicals, canonical)
}
sort.Strings(finalCanonicals)
if diff := schemaBaseCommandSetDiff(publicCanonicals, finalCanonicals); diff != "" {
t.Fatalf("final Schema tool set differs from public BoundCommandRegistry: %s", diff)
}
for _, canonical := range finalCanonicals {
canonical := canonical
t.Run(canonical, func(t *testing.T) {
tool := snapshot.Tools[canonical]
command, ok := bound.ByCanonical[canonical]
if !ok {
t.Fatalf("final Schema tool has no BoundCommand")
}
if command.Visibility != cli.SchemaVisibilityPublic {
t.Fatalf("final Schema tool binds non-public command visibility %q", command.Visibility)
}
if got := schemaBaseCommandString(tool["canonical_path"]); got != canonical {
t.Fatalf("final canonical_path = %q, want %q", got, canonical)
}
if got := schemaBaseCommandString(tool["primary_cli_path"]); got != command.PrimaryCLIPath {
t.Fatalf("final primary_cli_path = %q, want bound primary %q", got, command.PrimaryCLIPath)
}
if got := schemaBaseCommandString(tool["cli_path"]); got != command.PrimaryCLIPath {
t.Fatalf("final canonical view cli_path = %q, want bound primary %q", got, command.PrimaryCLIPath)
}
primaryMatch, err := resolveSchemaBaseCommandPath(root, command.PrimaryCLIPath)
if err != nil {
t.Fatalf("resolve primary path exactly: %v", err)
}
if primaryMatch.command == nil {
t.Fatalf("bound primary path %q does not exist in live Cobra tree", command.PrimaryCLIPath)
}
if primaryMatch.usedAlias {
t.Fatalf("bound primary path %q resolves through Cobra Aliases", command.PrimaryCLIPath)
}
if primaryMatch.command != command.PrimaryCommand {
t.Fatalf("bound primary pointer differs from exact live Cobra path %q", command.PrimaryCLIPath)
}
assertRunnableSchemaBaseCommand(t, command.PrimaryCommand, command.PrimaryCLIPath)
// Cobra dispatches a parsed --help flag to Help without invoking the
// command's Run/RunE or any business interface. Calling Help directly
// therefore exercises the same renderer without network side effects.
var help bytes.Buffer
command.PrimaryCommand.SetOut(&help)
command.PrimaryCommand.SetErr(&help)
if err := command.PrimaryCommand.Help(); err != nil {
t.Fatalf("render %q --help: %v", command.PrimaryCLIPath, err)
}
if strings.TrimSpace(help.String()) == "" {
t.Fatalf("%q --help rendered an empty document", command.PrimaryCLIPath)
}
wantAliases := append([]string(nil), command.Aliases...)
gotAliases := schemaBaseCommandStringSlice(tool["aliases"])
sort.Strings(wantAliases)
sort.Strings(gotAliases)
if diff := schemaBaseCommandSetDiff(wantAliases, gotAliases); diff != "" {
t.Fatalf("final aliases differ from BoundCommand: %s", diff)
}
boundAliases := make(map[string]cli.BoundAlias, len(command.AliasCommands))
for _, alias := range command.AliasCommands {
if _, duplicate := boundAliases[alias.Path]; duplicate {
t.Fatalf("BoundCommand has duplicate alias %q", alias.Path)
}
boundAliases[alias.Path] = alias
}
for _, aliasPath := range wantAliases {
alias, ok := boundAliases[aliasPath]
if !ok {
t.Fatalf("registry alias %q has no BoundAlias", aliasPath)
}
aliasMatch, err := resolveSchemaBaseCommandPath(root, aliasPath)
if err != nil {
t.Fatalf("resolve alias %q exactly: %v", aliasPath, err)
}
if aliasMatch.command == nil {
t.Fatalf("bound alias %q does not exist in live Cobra tree", aliasPath)
}
if aliasMatch.command != alias.Command {
t.Fatalf("BoundAlias pointer differs from exact live Cobra path %q", aliasPath)
}
assertRunnableSchemaBaseCommand(t, alias.Command, aliasPath)
switch alias.Kind {
case cli.AliasKindCobraAlias:
if !aliasMatch.usedAlias || alias.Command != command.PrimaryCommand {
t.Fatalf("Cobra alias %q must resolve through Aliases to the primary command pointer", aliasPath)
}
case cli.AliasKindCompatibilityLeaf:
if aliasMatch.usedAlias || alias.Command == command.PrimaryCommand {
t.Fatalf("compatibility alias %q must be a separate exact-name Cobra leaf", aliasPath)
}
default:
t.Fatalf("alias %q has unknown binding kind %q", aliasPath, alias.Kind)
}
if indexed, ok := bound.ByCLIPath[aliasPath]; !ok || indexed.CanonicalPath != canonical {
t.Fatalf("BoundCommandRegistry path index %q does not resolve to %q", aliasPath, canonical)
}
}
if len(boundAliases) != len(wantAliases) {
t.Fatalf("BoundCommand exposes %d alias bindings for %d reviewed aliases", len(boundAliases), len(wantAliases))
}
})
}
t.Logf("validated %d final Schema tools and their executable base commands", len(finalCanonicals))
}
func assertRunnableSchemaBaseCommand(t *testing.T, command *cobra.Command, path string) {
t.Helper()
if command == nil || !command.Runnable() || command.HasSubCommands() {
t.Fatalf("Schema path %q does not bind a runnable Cobra leaf", path)
}
}
type schemaBaseCommandPathMatch struct {
command *cobra.Command
usedAlias bool
}
// resolveSchemaBaseCommandPath independently resolves exact Cobra names and
// aliases for the delivery contract test. Like the production binder, it does
// not accept Cobra prefix matching or suggestions.
func resolveSchemaBaseCommandPath(root *cobra.Command, rawPath string) (schemaBaseCommandPathMatch, error) {
parts := strings.Fields(strings.TrimSpace(rawPath))
if len(parts) > 0 && root != nil && parts[0] == root.Name() {
parts = parts[1:]
}
if root == nil || len(parts) == 0 {
return schemaBaseCommandPathMatch{}, nil
}
current := root
usedAlias := false
for _, part := range parts {
exact := schemaBaseCommandChildrenNamed(current, part, false)
if len(exact) > 1 {
return schemaBaseCommandPathMatch{}, fmt.Errorf("command segment %q is ambiguous", part)
}
if len(exact) == 1 {
current = exact[0]
continue
}
aliases := schemaBaseCommandChildrenNamed(current, part, true)
if len(aliases) > 1 {
return schemaBaseCommandPathMatch{}, fmt.Errorf("alias segment %q is ambiguous", part)
}
if len(aliases) == 0 {
return schemaBaseCommandPathMatch{}, nil
}
current = aliases[0]
usedAlias = true
}
return schemaBaseCommandPathMatch{command: current, usedAlias: usedAlias}, nil
}
func schemaBaseCommandChildrenNamed(parent *cobra.Command, name string, aliases bool) []*cobra.Command {
var matches []*cobra.Command
for _, child := range parent.Commands() {
matched := child.Name() == name
if aliases {
matched = false
for _, alias := range child.Aliases {
if alias == name {
matched = true
break
}
}
}
if !matched {
continue
}
seen := false
for _, existing := range matches {
if existing == child {
seen = true
break
}
}
if !seen {
matches = append(matches, child)
}
}
return matches
}
func schemaBaseCommandString(value any) string {
text, _ := value.(string)
return strings.TrimSpace(text)
}
func schemaBaseCommandStringSlice(value any) []string {
var values []string
switch typed := value.(type) {
case []string:
values = append(values, typed...)
case []any:
for _, item := range typed {
if text, ok := item.(string); ok {
values = append(values, text)
}
}
}
for index := range values {
values[index] = strings.TrimSpace(values[index])
}
return values
}
func schemaBaseCommandSetDiff(want, got []string) string {
wantSet := make(map[string]bool, len(want))
gotSet := make(map[string]bool, len(got))
for _, value := range want {
wantSet[value] = true
}
for _, value := range got {
gotSet[value] = true
}
var missing, extra []string
for value := range wantSet {
if !gotSet[value] {
missing = append(missing, value)
}
}
for value := range gotSet {
if !wantSet[value] {
extra = append(extra, value)
}
}
sort.Strings(missing)
sort.Strings(extra)
if len(missing) == 0 && len(extra) == 0 && len(want) == len(got) {
return ""
}
return fmt.Sprintf("missing=%v extra=%v want_count=%d got_count=%d", missing, extra, len(want), len(got))
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
func TestRuntimeSchemaCompletenessCoversPublicCommandTree(t *testing.T) {
exclusions, err := cli.EmbeddedRuntimeSchemaExclusions()
if err != nil {
t.Fatal(err)
}
root := NewRootCommand()
if err := cli.ValidateEmbeddedRuntimeSchemaCompleteness(root); err != nil {
t.Fatal(err)
}
report := cli.RuntimeSchemaCompleteness(root, exclusions)
if len(report.Missing) > 0 || len(report.InvalidExclusions) > 0 || len(report.StaleExclusions) > 0 {
t.Fatalf("runtime schema completeness: missing=%v invalid=%v stale=%v", report.Missing, report.InvalidExclusions, report.StaleExclusions)
}
if !containsSchemaPath(report.Covered, "chat category create-smart") {
t.Fatal("chat category create-smart is not covered by runtime Schema")
}
if !containsSchemaPath(report.Excluded, "agoal strategy list") {
t.Fatal("agoal strategy list is not recorded as a reviewed exclusion")
}
}
func containsSchemaPath(paths []string, want string) bool {
for _, path := range paths {
if path == want {
return true
}
}
return false
}
+603
View File
@@ -0,0 +1,603 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
fullSchemaSnapshotOnce sync.Once
fullSchemaSnapshot cli.SchemaCatalogSnapshot
fullSchemaSnapshotErr error
)
// fullSchemaSnapshotForTest runs the complete source-to-delivery invariant
// once per test binary. The returned snapshot is a shared read-only fixture;
// callers still build their own Cobra root when they need executable command
// pointers. This preserves every full-Catalog assertion without paying the
// multi-gigabyte generation/validation cost three times under -race.
func fullSchemaSnapshotForTest(t testing.TB) cli.SchemaCatalogSnapshot {
t.Helper()
fullSchemaSnapshotOnce.Do(func() {
resolved, err := cli.ResolveSchemaBuild(NewRootCommand())
if err != nil {
fullSchemaSnapshotErr = err
return
}
fullSchemaSnapshot, fullSchemaSnapshotErr = cli.BuildSchemaCatalogSnapshot(resolved, cli.SchemaCatalogBuildOptions{})
})
if fullSchemaSnapshotErr != nil {
t.Fatalf("build shared final Schema snapshot: %v", fullSchemaSnapshotErr)
}
return fullSchemaSnapshot
}
func TestEmbeddedSchemaContractMapsToExecutableTree(t *testing.T) {
root := NewRootCommand()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatal(err)
}
expected := make(map[string]bool)
for _, command := range effective.Commands {
if command.Visibility == cli.SchemaVisibilityPublic {
expected[command.CanonicalPath] = true
}
}
var stdout, stderr bytes.Buffer
root.SetOut(&stdout)
root.SetErr(&stderr)
root.SetArgs([]string{"schema", "--all", "--format", "json"})
if err := root.Execute(); err != nil {
t.Fatalf("execute embedded schema --all: %v; stderr=%s", err, stderr.String())
}
var payload struct {
Products []struct {
Tools []struct {
CanonicalPath string `json:"canonical_path"`
} `json:"tools"`
} `json:"products"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("decode embedded schema --all: %v", err)
}
actual := make(map[string]bool)
var duplicates []string
for _, product := range payload.Products {
for _, tool := range product.Tools {
canonical := strings.TrimSpace(tool.CanonicalPath)
if canonical == "" {
t.Fatal("embedded schema --all contains an empty canonical path")
}
if actual[canonical] {
duplicates = append(duplicates, canonical)
}
actual[canonical] = true
}
}
if len(duplicates) > 0 {
sort.Strings(duplicates)
t.Fatalf("embedded schema --all contains duplicate canonicals: %v", duplicates)
}
var missing, extra []string
for canonical := range expected {
if !actual[canonical] {
missing = append(missing, canonical)
}
}
for canonical := range actual {
if !expected[canonical] {
extra = append(extra, canonical)
}
}
if len(missing) > 0 || len(extra) > 0 {
sort.Strings(missing)
sort.Strings(extra)
t.Fatalf("embedded Schema canonical set differs from EffectiveCommandRegistry: missing=%v extra=%v", missing, extra)
}
}
func TestGeneratedSchemaContractMapsToExecutableTree(t *testing.T) {
root := NewRootCommand()
snapshot := fullSchemaSnapshotForTest(t)
bindings, err := cli.EmbeddedSchemaParameterBindings()
if err != nil {
t.Fatalf("EmbeddedSchemaParameterBindings() error = %v", err)
}
if len(snapshot.Tools) == 0 {
t.Fatal("generated Schema Catalog contains no tools")
}
for canonicalPath := range bindings {
if _, ok := snapshot.Tools[canonicalPath]; !ok {
t.Errorf("parameter bindings reference canonical %q that is absent from the final generated Schema", canonicalPath)
}
}
for canonicalPath, definition := range snapshot.Tools {
cliPath := schemaContractString(definition["primary_cli_path"])
if cliPath == "" {
cliPath = schemaContractString(definition["cli_path"])
}
command := exactCommandForTest(root, cliPath)
if command == nil {
for _, alias := range schemaContractStringSlice(definition["aliases"]) {
if command = exactCommandForTest(root, alias); command != nil {
break
}
}
}
if command == nil {
t.Errorf("%s has no executable CLI path %q", canonicalPath, cliPath)
continue
}
for parameterName, rawParameter := range schemaContractMap(definition["parameters"]) {
flag := schemaContractCommandFlag(command, parameterName)
if flag == nil {
t.Errorf("%s maps parameter %q to missing flag on %q", canonicalPath, parameterName, command.CommandPath())
continue
}
if got, want := schemaContractFlagDefault(flag), schemaContractString(rawParameter["default"]); want != got {
t.Errorf("%s parameter %q default = %q, Cobra --help default = %q", canonicalPath, parameterName, want, got)
}
}
for flagName, propertyName := range bindings[canonicalPath] {
flag := schemaContractCommandFlag(command, flagName)
if flag == nil || flag.Hidden {
t.Errorf("%s binding --%s references a missing or hidden public flag", canonicalPath, flagName)
continue
}
parameter := schemaContractMap(definition["parameters"])[flagName]
if parameter == nil || schemaContractString(parameter["property"]) != propertyName {
t.Errorf("%s binding --%s -> %s is absent from generated Catalog", canonicalPath, flagName, propertyName)
}
}
}
}
func TestRuntimeSchemaParameterMetadataMapsToGeneratedCatalog(t *testing.T) {
snapshot := fullSchemaSnapshotForTest(t)
for canonicalPath, metadata := range cli.RuntimeSchemaParameterMetadataDefinitions() {
tool := snapshot.Tools[canonicalPath]
if tool == nil {
t.Errorf("parameter metadata references unknown tool %q", canonicalPath)
continue
}
parameters, _ := tool["parameters"].(map[string]any)
parameter := func(flagName string) map[string]any {
value, _ := parameters[flagName].(map[string]any)
if value == nil {
t.Errorf("%s parameter metadata references unknown flag --%s", canonicalPath, flagName)
}
return value
}
for _, flagName := range metadata.Inherited {
parameter(flagName)
}
for _, flagName := range metadata.Required {
if value := parameter(flagName); value != nil {
assertRuntimeSchemaMetadataCandidate(t, canonicalPath, flagName, value, "required", true)
}
}
for flagName, want := range metadata.RequiredWhen {
if value := parameter(flagName); value != nil {
assertRuntimeSchemaMetadataCandidate(t, canonicalPath, flagName, value, "required_when", want)
}
}
for flagName, want := range metadata.Formats {
if value := parameter(flagName); value != nil {
assertRuntimeSchemaMetadataCandidate(t, canonicalPath, flagName, value, "format", want)
}
}
for flagName, want := range metadata.Examples {
if value := parameter(flagName); value != nil {
assertRuntimeSchemaMetadataCandidate(t, canonicalPath, flagName, value, "example", want)
}
}
for flagName, want := range metadata.Enums {
if value := parameter(flagName); value != nil {
assertRuntimeSchemaMetadataCandidate(t, canonicalPath, flagName, value, "enum", want)
}
}
}
}
// assertRuntimeSchemaMetadataCandidate verifies the field-level resolver
// contract without assuming which source wins. Typed runtime metadata must
// remain visible as a candidate, resolution must select exactly one candidate,
// and the final payload/envelope must agree with that selected candidate.
func assertRuntimeSchemaMetadataCandidate(t testing.TB, canonicalPath, flagName string, parameter map[string]any, field string, typedValue any) {
t.Helper()
fieldProvenance, _ := parameter["field_provenance"].(map[string]any)
provenance, _ := fieldProvenance[field].(map[string]any)
if provenance == nil {
t.Errorf("%s --%s %s has no final resolver provenance", canonicalPath, flagName, field)
return
}
foundTypedCandidate := false
var selected map[string]any
selectedCount := 0
for _, candidate := range schemaContractObjectSlice(provenance["candidates"]) {
if candidate["source"] == "typed_parameter_metadata" && schemaContractJSONEqual(candidate["value"], typedValue) {
foundTypedCandidate = true
}
if isSelected, _ := candidate["selected"].(bool); isSelected {
selected = candidate
selectedCount++
}
}
if !foundTypedCandidate {
t.Errorf("%s --%s %s provenance has no typed_parameter_metadata candidate with value %#v", canonicalPath, flagName, field, typedValue)
}
if selectedCount != 1 {
t.Errorf("%s --%s %s provenance selected candidates = %d, want 1", canonicalPath, flagName, field, selectedCount)
return
}
if got, want := parameter[field], selected["value"]; !schemaContractJSONEqual(got, want) {
t.Errorf("%s --%s final %s = %#v, selected provenance value = %#v", canonicalPath, flagName, field, got, want)
}
if got, want := provenance["value"], selected["value"]; !schemaContractJSONEqual(got, want) {
t.Errorf("%s --%s %s provenance value = %#v, selected candidate value = %#v", canonicalPath, flagName, field, got, want)
}
if got, want := provenance["precedence"], selected["precedence"]; got != want {
t.Errorf("%s --%s %s provenance precedence = %#v, selected candidate precedence = %#v", canonicalPath, flagName, field, got, want)
}
}
func schemaContractJSONEqual(left, right any) bool {
leftJSON, leftErr := json.Marshal(left)
rightJSON, rightErr := json.Marshal(right)
return leftErr == nil && rightErr == nil && bytes.Equal(leftJSON, rightJSON)
}
func schemaContractObjectSlice(value any) []map[string]any {
switch typed := value.(type) {
case []map[string]any:
return typed
case []any:
out := make([]map[string]any, 0, len(typed))
for _, item := range typed {
if object, ok := item.(map[string]any); ok {
out = append(out, object)
}
}
return out
default:
return nil
}
}
func schemaContractFlagDefault(flag *pflag.Flag) string {
if flag == nil {
return ""
}
value := strings.TrimSpace(flag.DefValue)
if value == "" || value == "0s" || value == "[]" || value == "{}" {
return ""
}
switch flag.Value.Type() {
case "bool":
if value == "false" {
return ""
}
case "int", "int8", "int16", "int32", "int64", "float32", "float64":
if value == "0" {
return ""
}
}
return value
}
func schemaContractCommandFlag(command *cobra.Command, name string) *pflag.Flag {
if command == nil {
return nil
}
if flag := command.Flags().Lookup(name); flag != nil {
return flag
}
for current := command; current != nil; current = current.Parent() {
if flag := current.PersistentFlags().Lookup(name); flag != nil {
return flag
}
}
return nil
}
func schemaContractMap(value any) map[string]map[string]any {
switch typed := value.(type) {
case map[string]map[string]any:
return typed
case map[string]any:
out := make(map[string]map[string]any, len(typed))
for key, item := range typed {
if object, ok := item.(map[string]any); ok {
out[key] = object
}
}
return out
default:
return nil
}
}
func schemaContractString(value any) string {
switch typed := value.(type) {
case string:
return typed
case nil:
return ""
default:
return fmt.Sprint(typed)
}
}
func schemaContractStringSlice(value any) []string {
switch typed := value.(type) {
case []string:
return typed
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
if text, ok := item.(string); ok {
out = append(out, text)
}
}
return out
default:
return nil
}
}
// schemaContractPayloadForBoundCanonicals builds a small test fixture by
// selecting already validated BoundCommand entries before Schema assembly.
// Production generation deliberately has no post-assembly subset option: its
// delivered set must always equal the public EffectiveCommandRegistry set.
func schemaContractPayloadForBoundCanonicals(t *testing.T, root *cobra.Command, canonicals ...string) cli.SchemaSnapshotPayload {
t.Helper()
if _, err := cli.ApplyEmbeddedManualSchemaHints(root); err != nil {
t.Fatalf("apply manual Schema hints: %v", err)
}
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("build effective CommandRegistry: %v", err)
}
fixtureRegistry := cli.EffectiveCommandRegistry{Commands: make([]cli.CommandSpec, 0, len(canonicals))}
for _, canonical := range canonicals {
command, ok := effective.ByCanonical[canonical]
if !ok {
t.Fatalf("effective fixture has no canonical %s", canonical)
}
fixtureRegistry.Commands = append(fixtureRegistry.Commands, command)
}
fixture, err := cli.BindEffectiveCommandRegistry(root, fixtureRegistry)
if err != nil {
t.Fatalf("bind synthetic effective CommandRegistry: %v", err)
}
registry, err := cli.AssembleSchemaRegistryFromBound(fixture)
if err != nil {
t.Fatalf("assemble synthetic bound Schema registry: %v", err)
}
payload, err := registry.ToSnapshotPayload()
if err != nil {
t.Fatalf("render synthetic bound Schema registry: %v", err)
}
return payload
}
func TestChatSchemaSeparatesSendAndReply(t *testing.T) {
snapshot := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(),
"chat.send_personal_message",
"chat.reply_personal_message",
)
send, ok := snapshot.Tools["chat.send_personal_message"]
if !ok || schemaContractString(send["primary_cli_path"]) != "chat message send" {
t.Fatalf("send definition = %#v", send)
}
reply, ok := snapshot.Tools["chat.reply_personal_message"]
if !ok || schemaContractString(reply["primary_cli_path"]) != "chat message reply" {
t.Fatalf("reply definition = %#v", reply)
}
interfaceRef, _ := reply["interface_ref"].(map[string]any)
if schemaContractString(interfaceRef["product_id"]) != "chat" || schemaContractString(interfaceRef["rpc_name"]) != "send_personal_message" {
t.Fatalf("reply interface = %#v", interfaceRef)
}
if _, exists := snapshot.Tools["chat.upload_conversation_file"]; exists {
t.Fatal("downlined chat file upload must not be advertised in Schema")
}
}
func TestCalendarAttendeeDeleteSchemaMatchesRuntimeGate(t *testing.T) {
root := NewRootCommand()
snapshot := schemaContractPayloadForBoundCanonicals(t, root, "calendar.remove_calendar_participant")
tool := snapshot.Tools["calendar.remove_calendar_participant"]
// Runtime does not gate this path today; Schema confirmation must follow metadata.runtime_gate.
if got := tool["confirmation"]; got != "not_required" {
t.Fatalf("calendar attendee delete confirmation = %#v, want not_required", got)
}
if got := tool["risk"]; got != "medium" {
t.Fatalf("calendar attendee delete risk = %#v, want medium", got)
}
}
func TestPromptingWritesRequireUserConfirmation(t *testing.T) {
wantEffects := map[string]string{
"attendance.class_create": "write",
"attendance.class_update": "write",
"doc.delete_comment": "write",
"doc.version_revert": "write",
"drive.publish_set": "write",
"drive.publish_unset": "write",
"sheet.chart_delete": "write",
"sheet.delete_pivot_table": "write",
}
wantRisks := map[string]string{
"attendance.class_create": "medium",
"attendance.class_update": "medium",
"doc.delete_comment": "medium",
"doc.version_revert": "medium",
"drive.publish_set": "medium",
"drive.publish_unset": "medium",
"sheet.chart_delete": "medium",
"sheet.delete_pivot_table": "medium",
}
wantSources := map[string]string{
"attendance.class_create": "internal/cli/schema_hints/metadata/attendance.json",
"attendance.class_update": "internal/cli/schema_hints/metadata/attendance.json",
"doc.delete_comment": "internal/cli/schema_hints/metadata/doc.json",
"doc.version_revert": "internal/cli/schema_hints/metadata/doc.json",
"drive.publish_set": "internal/cli/schema_hints/metadata/drive.json",
"drive.publish_unset": "internal/cli/schema_hints/metadata/drive.json",
"sheet.chart_delete": "internal/cli/schema_hints/metadata/sheet.json",
"sheet.delete_pivot_table": "internal/cli/schema_hints/metadata/sheet.json",
}
canonicals := make([]string, 0, len(wantEffects))
for canonical := range wantEffects {
canonicals = append(canonicals, canonical)
}
sort.Strings(canonicals)
snapshot := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, canonical := range canonicals {
tool := snapshot.Tools[canonical]
if got := tool["effect"]; got != wantEffects[canonical] {
t.Errorf("%s effect = %#v, want %s", canonical, got, wantEffects[canonical])
}
if got := tool["risk"]; got != wantRisks[canonical] {
t.Errorf("%s risk = %#v, want %s", canonical, got, wantRisks[canonical])
}
if got := tool["confirmation"]; got != "user_required" {
t.Errorf("%s confirmation = %#v, want user_required", canonical, got)
}
provenance, _ := tool["field_provenance"].(map[string]any)
for _, field := range []string{"effect", "risk", "confirmation"} {
selected, _ := provenance[field].(map[string]any)
if got := selected["source"]; got != wantSources[canonical] {
t.Errorf("%s %s provenance source = %#v, want %s", canonical, field, got, wantSources[canonical])
}
}
}
}
func TestNewMainCommandInterfaceConversionsReachFinalSchema(t *testing.T) {
type conversion struct {
canonical string
flag string
cliType string
interfaceType string
}
wants := []conversion{
{canonical: "chat.list_message_favorites", flag: "size", cliType: "integer", interfaceType: "string"},
{canonical: "doc.update_comment", flag: "mention", cliType: "string", interfaceType: "array"},
{canonical: "sheet.create_pivot_table", flag: "properties", cliType: "string", interfaceType: "object"},
{canonical: "sheet.table_put", flag: "sheets", cliType: "string", interfaceType: "array"},
{canonical: "sheet.update_pivot_table", flag: "properties", cliType: "string", interfaceType: "object"},
}
canonicals := make([]string, 0, len(wants))
for _, want := range wants {
canonicals = append(canonicals, want.canonical)
}
snapshot := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, want := range wants {
tool := snapshot.Tools[want.canonical]
parameters, _ := tool["parameters"].(map[string]any)
parameter, _ := parameters[want.flag].(map[string]any)
if got := schemaContractString(parameter["type"]); got != want.cliType {
t.Errorf("%s --%s CLI type = %q, want %q", want.canonical, want.flag, got, want.cliType)
}
if got := schemaContractString(parameter["interface_type"]); got != want.interfaceType {
t.Errorf("%s --%s interface_type = %q, want %q", want.canonical, want.flag, got, want.interfaceType)
}
}
if got := snapshot.Tools["sheet.create_pivot_table"]["idempotency"]; got != "non_idempotent" {
t.Errorf("sheet.create_pivot_table idempotency = %#v, want non_idempotent", got)
}
}
func TestDefaultedPaginationSchemaFlagsAreOptional(t *testing.T) {
wants := map[string][]string{
"chat.search_messages_by_time_range": {"limit"},
"oa.list_user_visible_process": {"cursor", "limit"},
"report.get_received_report_list": {"cursor", "size"},
"todo.get_user_todos_in_current_org": {"page"},
}
canonicals := make([]string, 0, len(wants)+1)
for canonicalPath := range wants {
canonicals = append(canonicals, canonicalPath)
}
canonicals = append(canonicals, "aitable.section_reorder")
sort.Strings(canonicals)
snapshot := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for canonicalPath, flags := range wants {
parameters := schemaContractMap(snapshot.Tools[canonicalPath]["parameters"])
for _, flagName := range flags {
if parameters[flagName]["required"] == true {
t.Errorf("%s --%s has a CLI default and must remain optional", canonicalPath, flagName)
}
}
}
targetIndex := schemaContractMap(snapshot.Tools["aitable.section_reorder"]["parameters"])["target-index"]
if targetIndex["required"] != true {
t.Error("aitable.section_reorder --target-index uses -1 as a sentinel and must remain required")
}
}
func TestPATSchemaKeepsCLIContract(t *testing.T) {
root := NewRootCommand()
payload := schemaContractPayloadForBoundCanonicals(t, root, "pat.batch_grant")
tool := payload.Tools["pat.batch_grant"]
parameters, _ := tool["parameters"].(map[string]any)
grantType, _ := parameters["grant-type"].(map[string]any)
if grantType["default"] != "permanent" {
t.Fatalf("grant-type default = %#v", grantType["default"])
}
positionals, _ := tool["positionals"].([]any)
if len(positionals) != 1 {
t.Fatalf("PAT positionals = %#v", tool["positionals"])
}
positional, _ := positionals[0].(map[string]any)
if positional["name"] != "scope" || positional["variadic"] != true {
t.Fatalf("PAT positionals = %#v", tool["positionals"])
}
}
func exactCommandForTest(root *cobra.Command, path string) *cobra.Command {
parts := strings.Fields(strings.TrimSpace(path))
if len(parts) > 0 && parts[0] == root.Name() {
parts = parts[1:]
}
current := root
for _, name := range parts {
var next *cobra.Command
for _, child := range current.Commands() {
if child.Name() == name {
next = child
break
}
}
if next == nil {
return nil
}
current = next
}
if current == root {
return nil
}
return current
}
@@ -0,0 +1,304 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// TestFinalSchemaParametersMatchExecutableHelpFlags is the fast, in-process
// Help <-> Schema parameter completeness gate. It deliberately starts from the
// reviewed registry and its exact Cobra bindings, then compares every public
// primary leaf with the final delivered ToolSpec projection. The binder has
// already proved that reviewed compatibility leaves have the same executable
// contract as their primary, so aliases do not create a second parameter
// source here.
func TestFinalSchemaParametersMatchExecutableHelpFlags(t *testing.T) {
root := NewRootCommand()
bound := boundSchemaCommandsForHelpFlagTest(t, root)
snapshot := fullSchemaSnapshotForTest(t)
assertSchemaParametersMatchExecutableHelpFlags(t, bound, snapshot.Tools, "source-built final Schema")
}
// TestEmbeddedSchemaParametersMatchExecutableHelpFlags runs the same exact-set
// gate against the artifact that ships in the binary. Going through the real
// schema --all command is intentional: a stale generated Catalog must fail
// even when a fresh source-built snapshot would agree with Cobra Help.
func TestEmbeddedSchemaParametersMatchExecutableHelpFlags(t *testing.T) {
root := NewRootCommand()
bound := boundSchemaCommandsForHelpFlagTest(t, root)
tools := embeddedSchemaAllToolsForHelpFlagTest(t, root)
assertSchemaParametersMatchExecutableHelpFlags(t, bound, tools, "embedded schema --all")
}
func boundSchemaCommandsForHelpFlagTest(t testing.TB, root *cobra.Command) cli.BoundCommandRegistry {
t.Helper()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("build EffectiveCommandRegistry: %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("bind EffectiveCommandRegistry to live Cobra tree: %v", err)
}
return bound
}
func embeddedSchemaAllToolsForHelpFlagTest(t testing.TB, root *cobra.Command) map[string]map[string]any {
t.Helper()
var stdout, stderr bytes.Buffer
root.SetOut(&stdout)
root.SetErr(&stderr)
root.SetArgs([]string{"schema", "--all", "--format", "json"})
if err := root.Execute(); err != nil {
t.Fatalf("execute embedded schema --all: %v; stderr=%s", err, stderr.String())
}
var payload struct {
Products []struct {
Tools []map[string]any `json:"tools"`
} `json:"products"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("decode embedded schema --all: %v", err)
}
tools := make(map[string]map[string]any)
for _, product := range payload.Products {
for _, tool := range product.Tools {
canonical := strings.TrimSpace(schemaContractString(tool["canonical_path"]))
if canonical == "" {
t.Fatal("embedded schema --all contains an empty canonical path")
}
if _, exists := tools[canonical]; exists {
t.Fatalf("embedded schema --all contains duplicate canonical %q", canonical)
}
tools[canonical] = tool
}
}
if len(tools) == 0 {
t.Fatal("embedded schema --all contains no tools")
}
return tools
}
func assertSchemaParametersMatchExecutableHelpFlags(
t testing.TB,
bound cli.BoundCommandRegistry,
tools map[string]map[string]any,
source string,
) {
t.Helper()
problems := schemaHelpFlagCompletenessProblems(bound, tools)
checked := 0
for _, command := range bound.Commands {
if command.Visibility == cli.SchemaVisibilityPublic {
checked++
}
}
if len(problems) > 0 {
t.Fatalf("%s parameter surface differs from executable Cobra Help:\n%s", source, strings.Join(problems, "\n"))
}
t.Logf("validated Help-visible flags against %s parameters for %d public tools", source, checked)
}
func TestSchemaHelpFlagCompletenessRejectsStaleCatalogFlags(t *testing.T) {
leaf := &cobra.Command{Use: "run", Run: func(*cobra.Command, []string) {}}
leaf.Flags().String("fresh", "", "new executable input")
bound := cli.BoundCommandRegistry{Commands: []cli.BoundCommandSpec{{
CommandSpec: cli.CommandSpec{
CanonicalPath: "sample.run",
PrimaryCLIPath: "sample run",
Visibility: cli.SchemaVisibilityPublic,
},
PrimaryCommand: leaf,
}}}
tools := map[string]map[string]any{
"sample.run": {
"parameters": map[string]any{
"stale": map[string]any{"type": "string"},
},
},
}
problems := schemaHelpFlagCompletenessProblems(bound, tools)
joined := strings.Join(problems, "\n")
if !strings.Contains(joined, `missing_in_schema=["fresh"]`) ||
!strings.Contains(joined, `extra_in_schema=["stale"]`) {
t.Fatalf("stale Catalog flag drift was not reported: %s", joined)
}
}
func schemaHelpFlagCompletenessProblems(bound cli.BoundCommandRegistry, tools map[string]map[string]any) []string {
var problems []string
public := make(map[string]bool)
checked := 0
for _, command := range bound.Commands {
if command.Visibility != cli.SchemaVisibilityPublic {
continue
}
public[command.CanonicalPath] = true
checked++
tool, ok := tools[command.CanonicalPath]
if !ok {
problems = append(problems, fmt.Sprintf(
"canonical=%q path=%q missing final Schema tool",
command.CanonicalPath,
command.PrimaryCLIPath,
))
continue
}
if problem := schemaHelpFlagCompletenessProblem(
command.CanonicalPath,
command.PrimaryCLIPath,
command.PrimaryCommand,
tool,
); problem != "" {
problems = append(problems, problem)
}
}
for canonical := range tools {
if !public[canonical] {
problems = append(problems, fmt.Sprintf("canonical=%q is an unexpected final Schema tool", canonical))
}
}
if checked != len(tools) {
problems = append(problems, fmt.Sprintf(
"public BoundCommand count=%d final Schema tool count=%d",
checked,
len(tools),
))
}
sort.Strings(problems)
return problems
}
func TestSchemaHelpFlagCompletenessRejectsAncestorPersistentLeak(t *testing.T) {
root := &cobra.Command{Use: "dws"}
root.PersistentFlags().String("format", "json", "output format")
product := &cobra.Command{Use: "product"}
product.PersistentFlags().String("leaked", "", "product-scoped option")
leaf := &cobra.Command{Use: "run", Run: func(*cobra.Command, []string) {}}
leaf.Flags().String("declared", "", "declared option")
leaf.Flags().String("json", "", "Base JSON object payload for this tool invocation")
leaf.Flags().String("hidden", "", "internal option")
_ = leaf.Flags().MarkHidden("hidden")
root.AddCommand(product)
product.AddCommand(leaf)
tool := map[string]any{
"parameters": map[string]any{
"declared": map[string]any{"type": "string"},
},
}
problem := schemaHelpFlagCompletenessProblem("product.run", "product run", leaf, tool)
if !strings.Contains(problem, `missing_in_schema=["leaked"]`) {
t.Fatalf("ancestor persistent leak was not reported: %s", problem)
}
if strings.Contains(problem, "format") || strings.Contains(problem, "json") || strings.Contains(problem, "hidden") {
t.Fatalf("root controls and reviewed non-Schema flags must be excluded: %s", problem)
}
}
func schemaHelpFlagCompletenessProblem(canonical, path string, command *cobra.Command, tool map[string]any) string {
helpFlags := schemaHelpVisibleFlagNames(command)
schemaFlags := make(map[string]bool)
for name := range schemaContractMap(tool["parameters"]) {
schemaFlags[name] = true
}
missing := schemaFlagNameDifference(helpFlags, schemaFlags)
extra := schemaFlagNameDifference(schemaFlags, helpFlags)
if len(missing) == 0 && len(extra) == 0 {
return ""
}
return fmt.Sprintf(
"canonical=%q path=%q missing_in_schema=%s extra_in_schema=%s",
canonical,
path,
schemaQuotedFlagNames(missing),
schemaQuotedFlagNames(extra),
)
}
// schemaHelpVisibleFlagNames models Cobra's leaf Help surface without rendering
// text. Local flags and ancestor persistent flags are executable tool inputs.
// Only the reviewed root execution controls are omitted; an unexpected new
// root persistent flag must fail this gate instead of being silently treated as
// process scaffolding.
func schemaHelpVisibleFlagNames(command *cobra.Command) map[string]bool {
visible := make(map[string]bool)
if command == nil {
return visible
}
visit := func(flag *pflag.Flag, rootPersistent bool) {
if flag == nil || flag.Hidden || flag.Name == "help" || schemaGenericPayloadEscapeHatch(flag) {
return
}
if rootPersistent && schemaRootExecutionControl(flag.Name) {
return
}
visible[flag.Name] = true
}
command.LocalNonPersistentFlags().VisitAll(func(flag *pflag.Flag) { visit(flag, false) })
command.PersistentFlags().VisitAll(func(flag *pflag.Flag) { visit(flag, false) })
root := command.Root()
for parent := command.Parent(); parent != nil; parent = parent.Parent() {
isRoot := parent == root
parent.PersistentFlags().VisitAll(func(flag *pflag.Flag) { visit(flag, isRoot) })
}
return visible
}
func schemaRootExecutionControl(name string) bool {
switch name {
case "client-id", "client-secret", "debug", "dry-run", "fields", "format", "jq", "mock",
"output", "profile", "timeout", "token", "verbose", "yes":
return true
default:
return false
}
}
func schemaGenericPayloadEscapeHatch(flag *pflag.Flag) bool {
if flag == nil {
return false
}
switch flag.Name {
case "json":
return strings.TrimSpace(flag.Usage) == "Base JSON object payload for this tool invocation"
case "params":
return strings.TrimSpace(flag.Usage) == "Additional JSON object payload merged after --json"
default:
return false
}
}
func schemaFlagNameDifference(left, right map[string]bool) []string {
result := make([]string, 0)
for name := range left {
if !right[name] {
result = append(result, name)
}
}
sort.Strings(result)
return result
}
func schemaQuotedFlagNames(names []string) string {
quoted := make([]string, 0, len(names))
for _, name := range names {
quoted = append(quoted, fmt.Sprintf("%q", name))
}
return "[" + strings.Join(quoted, ",") + "]"
}
@@ -0,0 +1,330 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"encoding/json"
"os"
"sort"
"strings"
"testing"
)
func TestReviewedRoutedInterfacesReachFinalSchema(t *testing.T) {
type interfaceCase struct {
canonical string
mode string
reason string
sourceSuffix string
}
tests := []interfaceCase{
{
canonical: "attendance.get_attendance_summary",
mode: "composite",
reason: "Reviewed unpinned remote adapter: the CLI calls attendance-wukong/get_user_attendance_summary, which is absent from the pinned MCP metadata snapshot; the incompatible attendance/get_attendance_summary contract must not be advertised.",
sourceSuffix: "internal/cli/schema_hints/metadata/attendance.json",
},
{
canonical: "drive.list_files",
mode: "composite",
reason: "The CLI command routes by --workspace between drive/list_files and doc/list_nodes, so the reviewed executable wrapper has no single direct MCP interface.",
sourceSuffix: "internal/cli/schema_hints/metadata/drive.json",
},
{
canonical: "chat.search_groups",
mode: "composite",
reason: "Reviewed unpinned remote adapter: the CLI calls im/search_groups with a flat payload, while the pinned snapshot only contains the incompatible chat/search_groups_by_keyword contract.",
sourceSuffix: "internal/cli/schema_hints/metadata/chat.json",
},
{
canonical: "sheet.range_batch_set_style",
mode: "composite",
reason: "The CLI reads a local batch file and performs multiple sheet/update_range calls with local continue-on-error control; the workflow has no single direct MCP interface.",
sourceSuffix: "internal/cli/schema_hints/metadata/sheet.json",
},
{
canonical: "sheet.range_read",
mode: "composite",
reason: "Reviewed unpinned remote adapter: the CLI calls sheet/get_cell_infos, which is absent from the pinned MCP metadata snapshot; the incompatible sheet/get_range contract must not be advertised.",
sourceSuffix: "internal/cli/schema_hints/metadata/sheet.json",
},
{
canonical: "wiki.list_wikiSpaces",
mode: "composite",
reason: "The CLI command routes by --type between wiki/list_wikiSpaces and drive/list_spaces, so the reviewed executable wrapper has no single direct MCP interface.",
sourceSuffix: "internal/cli/schema_hints/metadata/wiki.json",
},
{
canonical: "event.consume",
mode: "composite",
reason: "Reviewed composite workflow: the command creates or reuses a remote personal-event subscription and coordinates the local event bus and Stream consumer; no single pinned RPC represents the workflow.",
sourceSuffix: "internal/cli/schema_hints/metadata/event.json",
},
{
canonical: "event.status",
mode: "composite",
reason: "Reviewed composite workflow: the command reads the remote personal-event subscription control plane and combines it with local bus and consumer state; no single pinned RPC represents the result.",
sourceSuffix: "internal/cli/schema_hints/metadata/event.json",
},
{
canonical: "event.stop",
mode: "composite",
reason: "Reviewed composite workflow: the command deletes remote personal-event subscriptions, interrupts local consumers, updates local state, and may stop the local bus; no single pinned RPC represents the workflow.",
sourceSuffix: "internal/cli/schema_hints/metadata/event.json",
},
}
for _, canonical := range []string{
"aitable.view_update_aggregate",
"aitable.view_update_card",
"aitable.view_update_field_widths",
"aitable.view_update_timebar",
} {
tests = append(tests, interfaceCase{
canonical: canonical,
mode: "composite",
reason: "The CLI performs an aitable/get_views preflight, locally transforms the requested configuration, and then calls aitable/update_view; the two-call workflow has no single direct MCP interface.",
sourceSuffix: "internal/cli/schema_hints/metadata/aitable.json",
})
}
canonicals := make([]string, 0, len(tests))
for _, test := range tests {
canonicals = append(canonicals, test.canonical)
}
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, test := range tests {
test := test
t.Run(test.canonical, func(t *testing.T) {
tool := payload.Tools[test.canonical]
if got := schemaContractString(tool["interface_mode"]); got != test.mode {
t.Errorf("interface_mode = %q, want %q", got, test.mode)
}
if got := schemaContractString(tool["availability"]); got != "available" {
t.Errorf("availability = %q, want available", got)
}
if tool["interface_ref"] != nil {
t.Errorf("interface_ref = %#v, want nil for %s wrapper", tool["interface_ref"], test.mode)
}
if got := schemaContractString(tool["interface_reason"]); got != test.reason {
t.Errorf("interface_reason = %q, want %q", got, test.reason)
}
provenance := schemaContractMap(tool["field_provenance"])
for _, field := range []string{"interface_mode", "availability", "interface_ref", "interface_reason"} {
entry := provenance[field]
if entry == nil {
t.Errorf("missing %s provenance", field)
continue
}
if got := schemaContractString(entry["precedence"]); got != "reviewed_explicit" {
t.Errorf("%s provenance precedence = %q, want reviewed_explicit", field, got)
}
if got := schemaContractString(entry["source"]); !strings.HasSuffix(got, test.sourceSuffix) {
t.Errorf("%s provenance source = %q, want suffix %q", field, got, test.sourceSuffix)
}
}
if got := provenance["interface_ref"]["value"]; got != nil {
t.Errorf("interface_ref provenance value = %#v, want explicit null", got)
}
})
}
}
func TestViewGetWrappersUsePinnedGetViewsInterface(t *testing.T) {
canonicals := []string{
"aitable.view_get_aggregate",
"aitable.view_get_card",
"aitable.view_get_field_widths",
"aitable.view_get_fill_color_rule",
"aitable.view_get_filter",
"aitable.view_get_group",
"aitable.view_get_sort",
"aitable.view_get_timebar",
"aitable.view_get_visible_fields",
}
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, canonical := range canonicals {
tool := payload.Tools[canonical]
if got := schemaContractString(tool["interface_mode"]); got != "mcp" {
t.Errorf("%s interface_mode = %q, want mcp", canonical, got)
}
if got := schemaContractString(tool["availability"]); got != "available" {
t.Errorf("%s availability = %q, want available", canonical, got)
}
ref := schemaInterfaceObject(tool["interface_ref"])
if product, rpc := schemaContractString(ref["product_id"]), schemaContractString(ref["rpc_name"]); product != "aitable" || rpc != "get_views" {
t.Errorf("%s interface_ref = %q/%q, want aitable/get_views", canonical, product, rpc)
}
if got := schemaContractString(tool["interface_reason"]); got != "" {
t.Errorf("%s interface_reason = %q, want empty for direct pinned interface", canonical, got)
}
parameters := schemaContractMap(tool["parameters"])
for flag, property := range map[string]string{
"base-id": "baseId",
"table-id": "tableId",
"view-id": "viewIds",
} {
if got := schemaContractString(parameters[flag]["property"]); got != property {
t.Errorf("%s --%s property = %q, want %q", canonical, flag, got, property)
}
}
provenance := schemaContractMap(tool["field_provenance"])
for _, field := range []string{"interface_mode", "availability", "interface_ref"} {
entry := provenance[field]
if got := schemaContractString(entry["precedence"]); got != "reviewed_explicit" {
t.Errorf("%s %s precedence = %q, want reviewed_explicit", canonical, field, got)
}
if got := schemaContractString(entry["source"]); !strings.Contains(got, "internal/cli/schema_hints/metadata/") {
t.Errorf("%s %s source = %q, want reviewed interface disposition source", canonical, field, got)
}
}
}
}
func TestReviewedInterfaceDispositionSourceOwnsRuntimeSurface(t *testing.T) {
type hintFile struct {
Source map[string]any `json:"source"`
Tools map[string]map[string]any `json:"tools"`
}
load := func(path string) hintFile {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var value hintFile
if err := json.Unmarshal(data, &value); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
return value
}
runtimeSurface := load("../cli/schema_hints/runtime-surface-completeness.json")
legacyDispositionKeys := load("../cli/schema_hints/zz-interface-disposition-review.json").Tools
dispositions := hintFile{Source: map[string]any{"reviewed": true}, Tools: map[string]map[string]any{}}
for _, product := range []string{
"attendance", "aitable", "chat", "drive", "event", "sheet", "wiki", "doc", "mail", "todo", "calendar", "conference", "contact", "dev", "devdoc", "ding", "live", "minutes", "oa", "pat", "report", "aisearch",
} {
path := "../cli/schema_hints/metadata/" + product + ".json"
if _, err := os.Stat(path); err != nil {
continue
}
file := load(path)
for canonical, hint := range file.Tools {
if _, ok := legacyDispositionKeys[canonical]; !ok {
continue
}
trimmed := map[string]any{}
for _, field := range []string{"interface_mode", "availability", "interface_ref", "interface_reason"} {
if value, exists := hint[field]; exists {
trimmed[field] = value
}
}
dispositions.Tools[canonical] = trimmed
}
}
if dispositions.Source["reviewed"] != true {
t.Fatalf("interface disposition source reviewed = %#v, want true", dispositions.Source["reviewed"])
}
for canonical, hint := range runtimeSurface.Tools {
if hint["reviewed"] != false {
t.Errorf("%s runtime surface reviewed = %#v, want false", canonical, hint["reviewed"])
}
for _, field := range []string{"interface_mode", "availability", "interface_ref", "interface_reason"} {
if _, exists := hint[field]; exists {
t.Errorf("%s runtime surface still owns %s", canonical, field)
}
}
if _, exists := dispositions.Tools[canonical]; !exists {
t.Errorf("%s runtime surface has no reviewed interface disposition", canonical)
}
}
allowedFields := map[string]bool{
"interface_mode": true,
"availability": true,
"interface_ref": true,
"interface_reason": true,
}
canonicals := make([]string, 0, len(dispositions.Tools))
for canonical, hint := range dispositions.Tools {
canonicals = append(canonicals, canonical)
for field := range hint {
if !allowedFields[field] {
t.Errorf("%s interface-only source contains non-interface field %s", canonical, field)
}
}
mode := schemaContractString(hint["interface_mode"])
if mode == "local" {
t.Errorf("%s remote interface review is incorrectly classified local", canonical)
}
if schemaContractString(hint["availability"]) != "available" {
t.Errorf("%s reviewed disposition is not available", canonical)
}
switch mode {
case "mcp":
ref := schemaInterfaceObject(hint["interface_ref"])
if schemaContractString(ref["product_id"]) == "" || schemaContractString(ref["rpc_name"]) == "" {
t.Errorf("%s reviewed mcp disposition has no complete interface_ref", canonical)
}
case "composite":
if hint["interface_ref"] != nil {
t.Errorf("%s reviewed composite disposition advertises interface_ref %#v", canonical, hint["interface_ref"])
}
if schemaContractString(hint["interface_reason"]) == "" {
t.Errorf("%s reviewed composite disposition has no reason", canonical)
}
default:
t.Errorf("%s reviewed disposition mode = %q", canonical, mode)
}
}
sort.Strings(canonicals)
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, canonical := range canonicals {
want := dispositions.Tools[canonical]
tool := payload.Tools[canonical]
if got := schemaContractString(tool["interface_mode"]); got != schemaContractString(want["interface_mode"]) {
t.Errorf("%s final interface_mode = %q, want %q", canonical, got, want["interface_mode"])
}
if got := schemaContractString(tool["availability"]); got != schemaContractString(want["availability"]) {
t.Errorf("%s final availability = %q, want %q", canonical, got, want["availability"])
}
if schemaContractString(want["interface_mode"]) == "composite" {
if tool["interface_ref"] != nil {
t.Errorf("%s final composite interface_ref = %#v, want nil", canonical, tool["interface_ref"])
}
if got := schemaContractString(tool["interface_reason"]); got != schemaContractString(want["interface_reason"]) {
t.Errorf("%s final interface_reason = %q, want %q", canonical, got, want["interface_reason"])
}
} else {
gotRef := schemaInterfaceObject(tool["interface_ref"])
wantRef := schemaInterfaceObject(want["interface_ref"])
for _, field := range []string{"product_id", "rpc_name"} {
if got := schemaContractString(gotRef[field]); got != schemaContractString(wantRef[field]) {
t.Errorf("%s final interface_ref.%s = %q, want %q", canonical, field, got, wantRef[field])
}
}
}
provenance := schemaContractMap(tool["field_provenance"])
fields := []string{"interface_mode", "availability", "interface_ref"}
if schemaContractString(want["interface_mode"]) == "composite" {
fields = append(fields, "interface_reason")
}
for _, field := range fields {
entry := provenance[field]
if got := schemaContractString(entry["precedence"]); got != "reviewed_explicit" {
t.Errorf("%s final %s precedence = %q, want reviewed_explicit", canonical, field, got)
}
if got := schemaContractString(entry["source"]); !strings.Contains(got, "internal/cli/schema_hints/metadata/") {
t.Errorf("%s final %s source = %q, want reviewed disposition source", canonical, field, got)
}
}
}
}
func schemaInterfaceObject(value any) map[string]any {
object, _ := value.(map[string]any)
return object
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"io"
"os"
"os/exec"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
const schemaLazyStartupChildEnv = "DWS_SCHEMA_LAZY_STARTUP_CHILD"
// TestOrdinaryRootCommandsDoNotLoadSchemaMetadata uses a fresh process so its
// counters describe package init, root construction, help, and version only;
// unrelated Schema tests cannot have initialized the snapshots first.
func TestOrdinaryRootCommandsDoNotLoadSchemaMetadata(t *testing.T) {
if os.Getenv(schemaLazyStartupChildEnv) == "1" {
assertSchemaMetadataNotLoaded(t, "package init")
root := NewRootCommand()
assertSchemaMetadataNotLoaded(t, "NewRootCommand")
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{"--help"})
if err := root.Execute(); err != nil {
t.Fatalf("root --help: %v", err)
}
assertSchemaMetadataNotLoaded(t, "root --help")
root = NewRootCommand()
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{"version"})
if err := root.Execute(); err != nil {
t.Fatalf("dws version: %v", err)
}
assertSchemaMetadataNotLoaded(t, "dws version")
return
}
command := exec.Command(os.Args[0], "-test.run=^TestOrdinaryRootCommandsDoNotLoadSchemaMetadata$", "-test.count=1")
command.Env = append(os.Environ(), schemaLazyStartupChildEnv+"=1")
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("lazy startup child failed: %v\n%s", err, strings.TrimSpace(string(output)))
}
}
func assertSchemaMetadataNotLoaded(t *testing.T, stage string) {
t.Helper()
if counts := cli.RuntimeSchemaMetadataLoadCounts(); counts != (cli.SchemaMetadataLoadCounts{}) {
t.Fatalf("%s loaded Schema metadata: %#v", stage, counts)
}
}
@@ -0,0 +1,146 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"reflect"
"sort"
"testing"
)
func TestEventRegistryDeliversOneTypedSchemaPath(t *testing.T) {
paths := map[string]string{
"event.consume": "event consume",
"event.list": "event list",
"event.schema": "event schema",
"event.status": "event status",
"event.stop": "event stop",
}
wantModes := map[string]string{
"event.consume": "composite",
"event.list": "local",
"event.schema": "local",
"event.status": "composite",
"event.stop": "composite",
}
canonicals := make([]string, 0, len(paths))
for canonical := range paths {
canonicals = append(canonicals, canonical)
}
sort.Strings(canonicals)
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
if len(payload.Tools) != len(paths) {
t.Fatalf("event fixture tools = %d, want %d", len(payload.Tools), len(paths))
}
if _, exists := payload.Tools["event._bus"]; exists {
t.Fatal("hidden event _bus leaked into final Schema")
}
for canonical, primary := range paths {
tool := payload.Tools[canonical]
if tool == nil {
t.Errorf("missing event tool %s", canonical)
continue
}
if got := schemaContractString(tool["primary_cli_path"]); got != primary {
t.Errorf("%s primary path = %q, want %q", canonical, got, primary)
}
if tool["interface_mode"] != wantModes[canonical] || tool["availability"] != "available" {
t.Errorf("%s interface disposition = %v/%v, want %s/available", canonical, tool["interface_mode"], tool["availability"], wantModes[canonical])
}
if schemaContractString(tool["interface_reason"]) == "" {
t.Errorf("%s has no reviewed interface reason", canonical)
}
}
consume := payload.Tools["event.consume"]
consumeParams := schemaContractMap(consume["parameters"])
for _, hiddenOrGlobal := range []string{"as", "debug", "help", "profile", "timeout", "yes"} {
if _, exists := consumeParams[hiddenOrGlobal]; exists {
t.Errorf("event.consume exposes hidden/global flag --%s as a tool parameter", hiddenOrGlobal)
}
}
for flag, wantType := range map[string]string{
"dry-run": "boolean",
"duration": "string",
"event-types": "array",
"max-events": "integer",
} {
if got := schemaContractString(consumeParams[flag]["type"]); got != wantType {
t.Errorf("event.consume --%s type = %q, want %q", flag, got, wantType)
}
}
if _, exists := consumeParams["duration"]["default"]; exists {
t.Error("event.consume --duration leaked zero default 0s")
}
if consume["dry_run"] != nil {
t.Errorf("event.consume declares an audited dry_run capability without a deterministic clean-environment preview: %#v", consume["dry_run"])
}
assertSchemaContractPositional(t, consume, "event_key", false)
assertSchemaContractConstraintGroup(t, consume, "require_one_of", []string{"event_key", "subscribe-id"})
eventSchema := payload.Tools["event.schema"]
assertSchemaContractPositional(t, eventSchema, "event_key", true)
stop := payload.Tools["event.stop"]
if stop["effect"] != "destructive" || stop["risk"] != "high" || stop["confirmation"] != "user_required" {
t.Errorf("event.stop safety = effect:%v risk:%v confirmation:%v", stop["effect"], stop["risk"], stop["confirmation"])
}
dryRun, _ := stop["dry_run"].(map[string]any)
if dryRun["preview_kind"] != "request" {
t.Errorf("event.stop dry_run = %#v, want request preview", stop["dry_run"])
}
assertSchemaContractPositional(t, stop, "subscribe_id", false)
assertSchemaContractConstraintGroup(t, stop, "require_one_of", []string{"all", "subscribe_id"})
assertSchemaContractConstraintGroup(t, stop, "mutually_exclusive", []string{"all", "subscribe_id"})
}
func TestDevDocSearchBindingAndRequiredAlternativesReachFinalSchema(t *testing.T) {
canonicals := []string{"dev.search_open_platform_docs_rag", "devdoc.search_open_platform_docs_rag"}
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, canonical := range canonicals {
tool := payload.Tools[canonical]
query := schemaContractMap(tool["parameters"])["query"]
if got := schemaContractString(query["property"]); got != "keyword" {
t.Errorf("%s --query property = %q, want keyword", canonical, got)
}
if query["required"] != false {
t.Errorf("%s --query required = %#v, want false because positional keyword is an alternative", canonical, query["required"])
}
assertSchemaContractConstraintGroup(t, tool, "require_one_of", []string{"query", "keyword"})
}
}
func assertSchemaContractPositional(t *testing.T, tool map[string]any, name string, required bool) {
t.Helper()
positionals, _ := tool["positionals"].([]any)
for _, raw := range positionals {
positional, _ := raw.(map[string]any)
if positional["name"] == name {
if positional["required"] != required {
t.Errorf("positional %s required = %#v, want %v", name, positional["required"], required)
}
return
}
}
t.Errorf("missing positional %s in %#v", name, tool["positionals"])
}
func assertSchemaContractConstraintGroup(t *testing.T, tool map[string]any, kind string, want []string) {
t.Helper()
constraints, _ := tool["constraints"].(map[string]any)
groups, _ := constraints[kind].([]any)
for _, rawGroup := range groups {
rawValues, _ := rawGroup.([]any)
values := make([]string, 0, len(rawValues))
for _, value := range rawValues {
if text, ok := value.(string); ok {
values = append(values, text)
}
}
if reflect.DeepEqual(values, want) {
return
}
}
t.Errorf("constraint %s lacks group %v: %#v", kind, want, constraints[kind])
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
// Keep the folder-list wrapper and the free-form KQL search command as two
// independent Agent contracts. The list command translates --folder-id into a
// query before calling the same RPC, so treating it as a search alias loses a
// real executable parameter surface.
func TestMailListAndSearchRemainDistinctRegistryCommands(t *testing.T) {
root := NewRootCommand()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("BuildEffectiveCommandRegistry() error = %v", err)
}
list, listOK := effective.ByCanonical["mail.list_emails"]
search, searchOK := effective.ByCanonical["mail.search_emails"]
if !listOK || !searchOK {
t.Fatalf("mail registry split missing: list=%t search=%t", listOK, searchOK)
}
if list.PrimaryCLIPath != "mail message list" || search.PrimaryCLIPath != "mail message search" {
t.Fatalf("mail registry paths: list=%q search=%q", list.PrimaryCLIPath, search.PrimaryCLIPath)
}
if len(list.Aliases) != 0 || len(search.Aliases) != 0 {
t.Fatalf("mail list/search must not alias each other: list=%v search=%v", list.Aliases, search.Aliases)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
listCommand := bound.ByCanonical[list.CanonicalPath].PrimaryCommand
searchCommand := bound.ByCanonical[search.CanonicalPath].PrimaryCommand
if listCommand == nil || searchCommand == nil || listCommand == searchCommand {
t.Fatalf("mail list/search Cobra bindings are not distinct: list=%p search=%p", listCommand, searchCommand)
}
if listCommand.Flags().Lookup("folder-id") == nil || listCommand.Flags().Lookup("query") != nil {
t.Fatal("mail list Cobra surface must expose --folder-id and not --query")
}
if searchCommand.Flags().Lookup("query") == nil || searchCommand.Flags().Lookup("folder-id") != nil {
t.Fatal("mail search Cobra surface must expose --query and not --folder-id")
}
}
@@ -0,0 +1,48 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
func TestMinutesFixedScopeLeavesBindToDistinctRegistryTools(t *testing.T) {
root := NewRootCommand()
effective, err := cli.BuildEffectiveCommandRegistry(root)
if err != nil {
t.Fatalf("build EffectiveCommandRegistry: %v", err)
}
bound, err := cli.BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("bind EffectiveCommandRegistry: %v", err)
}
want := map[string]string{
"minutes list all": "minutes.list_accessible_minutes",
"minutes list mine": "minutes.list_by_keyword_and_time_range",
"minutes list shared": "minutes.list_shared_minutes",
}
commands := map[any]bool{}
canonicals := map[string]bool{}
for path, canonical := range want {
command, ok := bound.ByCLIPath[path]
if !ok {
t.Errorf("bound registry path %q is missing", path)
continue
}
if command.CanonicalPath != canonical {
t.Errorf("bound registry path %q canonical = %q, want %q", path, command.CanonicalPath, canonical)
}
if command.PrimaryCLIPath != path || len(command.Aliases) != 0 || len(command.AliasCommands) != 0 {
t.Errorf("bound registry path %q retained alias navigation: primary=%q aliases=%v bound_aliases=%v", path, command.PrimaryCLIPath, command.Aliases, command.AliasCommands)
}
commands[command.PrimaryCommand] = true
canonicals[command.CanonicalPath] = true
}
if len(commands) != len(want) || len(canonicals) != len(want) {
t.Fatalf("minutes fixed-scope leaves collapsed: command pointers=%d canonicals=%d, want %d each", len(commands), len(canonicals), len(want))
}
}
@@ -0,0 +1,127 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"encoding/json"
"reflect"
"testing"
)
func TestSchemaReviewedInputMetadataUsesExecutableShapes(t *testing.T) {
canonicals := []string{
"aitable.field_update",
"attendance.adjustment_search",
"attendance.approve_list",
"attendance.group_search",
"attendance.overtime_search",
"attendance.selfsetting_get",
"attendance.vacation_update_type",
"chat.list_owned_or_admin_groups",
"sheet.batch_update",
"sheet.chart_create",
"sheet.chart_update",
"sheet.create_pivot_table",
"sheet.update_pivot_table",
"sheet.range_batch_clear",
"todo.add_todo_reminder",
"todo.get_user_todos_in_current_org",
}
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
optional := false
parameterCases := []struct {
canonical string
flag string
format string
enum []string
noEnum bool
example string
required *bool
requiredWhen string
jsonKind byte
}{
{canonical: "attendance.approve_list", flag: "start", format: "date"},
{canonical: "attendance.approve_list", flag: "end", format: "date"},
{canonical: "attendance.approve_list", flag: "types", noEnum: true, example: "overtime,leave"},
{canonical: "attendance.adjustment_search", flag: "page", required: &optional},
{canonical: "attendance.adjustment_search", flag: "limit", required: &optional},
{canonical: "attendance.group_search", flag: "page", required: &optional},
{canonical: "attendance.group_search", flag: "limit", required: &optional},
{canonical: "attendance.overtime_search", flag: "page", required: &optional},
{canonical: "attendance.overtime_search", flag: "limit", required: &optional},
{canonical: "attendance.selfsetting_get", flag: "setting-scene", enum: []string{
"checkRemind", "fastCheck", "checkResultNotify", "lackRemind",
"personalAttendStatNotify", "bossAttendStatNotify",
}},
{canonical: "chat.list_owned_or_admin_groups", flag: "role", enum: []string{"OWNER", "ADMIN"}, required: &optional},
{canonical: "chat.list_owned_or_admin_groups", flag: "limit", required: &optional},
{canonical: "sheet.batch_update", flag: "operations", format: "json", jsonKind: '['},
{canonical: "sheet.chart_create", flag: "properties", format: "json", jsonKind: '{'},
{canonical: "sheet.chart_update", flag: "properties", format: "json", jsonKind: '{'},
{canonical: "sheet.create_pivot_table", flag: "properties", format: "json", jsonKind: '{'},
{canonical: "sheet.update_pivot_table", flag: "properties", format: "json", jsonKind: '{'},
{canonical: "sheet.range_batch_clear", flag: "ranges", format: "json", jsonKind: '['},
{canonical: "todo.add_todo_reminder", flag: "base-time", enum: []string{"dueTime", "customTime"}},
{canonical: "todo.add_todo_reminder", flag: "due-date-offset", example: "-30", requiredWhen: "base-time is dueTime"},
{canonical: "todo.add_todo_reminder", flag: "reminder-time-stamp", example: "2026-03-10T18:00:00+08:00", requiredWhen: "base-time is customTime"},
{canonical: "todo.get_user_todos_in_current_org", flag: "role-types", noEnum: true, example: "creator,executor"},
}
for _, test := range parameterCases {
t.Run(test.canonical+"/"+test.flag, func(t *testing.T) {
tool := payload.Tools[test.canonical]
parameter := schemaContractMap(tool["parameters"])[test.flag]
if test.format != "" && parameter["format"] != test.format {
t.Fatalf("format = %#v, want %q", parameter["format"], test.format)
}
if test.enum != nil {
if got := schemaContractStringSlice(parameter["enum"]); !reflect.DeepEqual(got, test.enum) {
t.Fatalf("enum = %#v, want %#v", got, test.enum)
}
}
if test.noEnum {
if got := schemaContractStringSlice(parameter["enum"]); len(got) != 0 {
t.Fatalf("enum = %#v, want no scalar enum for a CSV parameter", got)
}
}
if test.example != "" && parameter["example"] != test.example {
t.Fatalf("example = %#v, want %q", parameter["example"], test.example)
}
if test.required != nil && parameter["required"] != *test.required {
t.Fatalf("required = %#v, want %v", parameter["required"], *test.required)
}
if test.requiredWhen != "" && parameter["required_when"] != test.requiredWhen {
t.Fatalf("required_when = %#v, want %q", parameter["required_when"], test.requiredWhen)
}
if test.jsonKind != 0 {
example, ok := parameter["example"].(string)
if !ok || example == "" {
t.Fatalf("example = %#v, want non-empty JSON", parameter["example"])
}
var decoded any
if err := json.Unmarshal([]byte(example), &decoded); err != nil {
t.Fatalf("example is invalid JSON: %v", err)
}
if example[0] != test.jsonKind {
t.Fatalf("example = %s, want JSON kind %q", example, test.jsonKind)
}
switch value := decoded.(type) {
case []any:
if len(value) == 0 {
t.Fatal("example must not be an empty array")
}
case map[string]any:
if len(value) == 0 {
t.Fatal("example must not be an empty object")
}
}
}
})
}
wantUpdateFields := []string{"name", "unit", "paid", "per-hours", "when-can-leave", "visibility-rules"}
assertSchemaContractConstraintGroup(t, payload.Tools["attendance.vacation_update_type"], "require_one_of", wantUpdateFields)
assertSchemaContractConstraintGroup(t, payload.Tools["aitable.field_update"], "require_one_of", []string{"name", "config", "ai-config"})
}
@@ -0,0 +1,91 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"sort"
"testing"
)
type finalSchemaSafetyWant struct {
canonical string
effect string
risk string
confirmation string
idempotency string
}
func TestReviewedMutationSafetyReachesFinalSchema(t *testing.T) {
wants := []finalSchemaSafetyWant{
{canonical: "aitable.form_field_hide", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "idempotent"},
{canonical: "chat.dismiss_group", effect: "destructive", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "drive.recycle_restore", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "unknown"},
{canonical: "minutes.create_speaker_summary", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "unknown"},
{canonical: "sheet.clear_range", effect: "write", risk: "medium", confirmation: "user_required", idempotency: "unknown"},
{canonical: "sheet.batch_update", effect: "write", risk: "medium", confirmation: "user_required", idempotency: "unknown"},
{canonical: "sheet.range_batch_clear", effect: "write", risk: "medium", confirmation: "user_required", idempotency: "unknown"},
{canonical: "sheet.group_dimension", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "unknown"},
{canonical: "sheet.sort_filter", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "unknown"},
{canonical: "sheet.ungroup_dimension", effect: "write", risk: "medium", confirmation: "not_required", idempotency: "unknown"},
}
assertFinalSchemaSafety(t, wants)
}
func TestDevAppWriteGuardRequiresFinalSchemaConfirmation(t *testing.T) {
wants := []finalSchemaSafetyWant{
{canonical: "dev.add_dev_app_members", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.apply_dev_app_permissions", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.create_dev_app", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.create_dev_app_version", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.delete_dev_app", effect: "destructive", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.disable_dev_app", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.disable_dev_app_robot", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.enable_dev_app", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.enable_dev_app_robot", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.publish_dev_app_version", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.remove_dev_app_members", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.remove_dev_app_permissions", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.set_extension_robot_config", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.set_extension_webapp_config", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.submit_robot_create_task", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.subscribe_dev_app_events", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.unsubscribe_dev_app_events", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.update_dev_app", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
{canonical: "dev.update_dev_app_security_config", effect: "write", risk: "high", confirmation: "user_required", idempotency: "unknown"},
}
assertFinalSchemaSafety(t, wants)
}
func assertFinalSchemaSafety(t *testing.T, wants []finalSchemaSafetyWant) {
t.Helper()
canonicals := make([]string, 0, len(wants))
for _, want := range wants {
canonicals = append(canonicals, want.canonical)
}
sort.Strings(canonicals)
payload := schemaContractPayloadForBoundCanonicals(t, NewRootCommand(), canonicals...)
for _, want := range wants {
want := want
t.Run(want.canonical, func(t *testing.T) {
tool := payload.Tools[want.canonical]
values := map[string]string{
"effect": want.effect,
"risk": want.risk,
"confirmation": want.confirmation,
"idempotency": want.idempotency,
}
provenance := schemaContractMap(tool["field_provenance"])
for field, expected := range values {
if got := schemaContractString(tool[field]); got != expected {
t.Errorf("%s = %q, want %q", field, got, expected)
}
if got := schemaContractString(provenance[field]["precedence"]); got != "reviewed_explicit" {
t.Errorf("%s provenance precedence = %q, want reviewed_explicit", field, got)
}
}
})
}
}
@@ -0,0 +1,81 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package app
import (
"sort"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/spf13/cobra"
)
// TestSheetFinalSchemaConfirmationMatchesRuntimeGuards closes the delivery
// invariant from the final typed Schema to the executable Cobra leaf. The
// runtime marker can only be installed by the command-local wrapper that also
// executes the typed confirmation guard.
func TestSheetFinalSchemaConfirmationMatchesRuntimeGuards(t *testing.T) {
snapshot := fullSchemaSnapshotForTest(t)
root := NewRootCommand()
schemaPaths := make(map[string]string)
for canonical, tool := range snapshot.Tools {
primaryPath := schemaContractString(tool["primary_cli_path"])
if primaryPath == "" {
primaryPath = schemaContractString(tool["cli_path"])
}
if !strings.HasPrefix(primaryPath, "sheet ") || schemaContractString(tool["confirmation"]) != "user_required" {
continue
}
if previous := schemaPaths[primaryPath]; previous != "" {
t.Fatalf("final Schema maps both %q and %q to Sheet path %q", previous, canonical, primaryPath)
}
schemaPaths[primaryPath] = canonical
command := exactCommandForTest(root, primaryPath)
if command == nil {
t.Errorf("%s final Schema path %q has no executable Cobra leaf", canonical, primaryPath)
continue
}
if !helpers.HasSheetMutationConfirmationGuard(command) {
t.Errorf("%s (%s) declares confirmation=user_required but has no command-local runtime guard", canonical, primaryPath)
}
}
if len(schemaPaths) == 0 {
t.Fatal("final Schema contains no Sheet confirmation=user_required leaves")
}
guardedPaths := make(map[string]bool)
rootPrefix := root.CommandPath() + " "
var visit func(*cobra.Command)
visit = func(command *cobra.Command) {
if helpers.HasSheetMutationConfirmationGuard(command) {
path := strings.TrimPrefix(command.CommandPath(), rootPrefix)
guardedPaths[path] = true
}
for _, child := range command.Commands() {
visit(child)
}
}
visit(root)
var missingGuards, undeclaredGuards []string
for path, canonical := range schemaPaths {
if !guardedPaths[path] {
missingGuards = append(missingGuards, canonical+" ("+path+")")
}
}
for path := range guardedPaths {
if schemaPaths[path] == "" {
undeclaredGuards = append(undeclaredGuards, path)
}
}
if len(missingGuards) != 0 || len(undeclaredGuards) != 0 {
sort.Strings(missingGuards)
sort.Strings(undeclaredGuards)
t.Fatalf("final Sheet Schema confirmation set differs from command-local runtime guards: missing=%v undeclared=%v", missingGuards, undeclaredGuards)
}
}
+2 -2
View File
@@ -64,7 +64,7 @@ skill 源默认取二进制内嵌的版本(升级二进制即升级 skill)
dws skill setup --mode mono --yes # 非交互装 mono
dws skill setup --mode multi --target claude # multi 全装到 ~/.claude/skills/
dws skill setup --mode multi -s aitable -s calendar # 只装 aitable + calendar
dws skill setup --mode multi -x live -x devdoc # 装其余 20 个,剔除 2 个
dws skill setup --mode multi -x live -x devdoc # 安装除 live、devdoc 外的其余 skill
dws skill setup --source /path/to/repo # 显式指定 skill 源`,
DisableAutoGenTag: true,
RunE: runSkillSetup,
@@ -505,7 +505,7 @@ func confirmSkillSetup(out io.Writer, mode, src string, dests []string, multiSki
if mode == skillSetupModeMulti {
fmt.Fprintln(out, "\n🧪 ─────────────────────────────────────────────────────────────")
fmt.Fprintln(out, " multi 模式当前为 EXPERIMENTAL(试验版 / Preview)")
fmt.Fprintln(out, " · 22 个 dingtalk-* 子 skill 跑过 verifier,可用但未达 stable")
fmt.Fprintf(out, " · 当前选择的 %d 个独立 skill 均跑过 verifier,可用但未达 stable\n", len(multiSkillNames))
fmt.Fprintln(out, " · 跨 skill 引用、bundle 命名、目录布局后续可能调整")
fmt.Fprintln(out, " · 不建议在生产 / 共享环境直接落地;问题请提 issue 反馈")
fmt.Fprintln(out, " 稳定版请用 --mode mono")
+34
View File
@@ -44,6 +44,11 @@ func TestMaterializeEmbeddedSkillSourceMono(t *testing.T) {
t.Errorf("expected embedded skill to contain %s: %v", rel, err)
}
}
if _, err := os.Stat(filepath.Join(dir, "schema-hints")); err == nil {
t.Fatal("embedded mono skill must not contain build-only schema-hints")
} else if !os.IsNotExist(err) {
t.Fatalf("stat embedded mono schema-hints: %v", err)
}
// cleanup must actually remove the temp dir.
cleanup()
@@ -52,6 +57,35 @@ func TestMaterializeEmbeddedSkillSourceMono(t *testing.T) {
}
}
// TestMaterializeEmbeddedSkillSourceMulti verifies that the peer multi bundle
// contains both the shared routing skill and the PAT product skill. Structured
// Schema hints are build inputs and must not become a third installable mode.
func TestMaterializeEmbeddedSkillSourceMulti(t *testing.T) {
dir, cleanup, err := materializeEmbeddedSkillSource(skillSetupModeMulti)
if err != nil {
t.Fatalf("materializeEmbeddedSkillSource: %v", err)
}
defer cleanup()
if !isSkillSourceRoot(dir, skillSetupModeMulti) {
t.Fatalf("extracted dir %s is not a valid multi skill source root", dir)
}
for _, rel := range []string{
filepath.Join("dws-shared", "SKILL.md"),
filepath.Join("dingtalk-pat", "SKILL.md"),
filepath.Join("dingtalk-pat", "references", "pat.md"),
} {
if _, err := os.Stat(filepath.Join(dir, rel)); err != nil {
t.Errorf("expected embedded multi skill to contain %s: %v", rel, err)
}
}
if _, err := os.Stat(filepath.Join(dir, "schema-hints")); err == nil {
t.Fatal("embedded multi skill must not contain build-only schema-hints")
} else if !os.IsNotExist(err) {
t.Fatalf("stat embedded multi schema-hints: %v", err)
}
}
// TestResolveSkillSetupSourceOrEmbeddedFallsBackToEmbedded verifies that with
// no --source and no DWS_SKILL_SOURCE, resolution uses the embedded bundle
// rather than probing the current working directory (the stale-skill footgun).
+20 -4
View File
@@ -15,6 +15,7 @@ package app
import (
"context"
"fmt"
"log/slog"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
@@ -36,6 +37,21 @@ func newToolCallerAdapter(runner executor.Runner, flags *GlobalFlags) edition.To
func (a *toolCallerAdapter) CallTool(ctx context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
inv := executor.NewHelperInvocation("overlay."+productID+"."+toolName, productID, toolName, args)
// Defense in depth for direct helper callers: global dry-run must never
// reach an injected/real Runner, even if a command bypasses the normal
// Schema leaf wrapper. EchoRunner produces the same stable dry_run envelope
// without catalog, auth, Keychain, endpoint or transport access.
if a != nil && a.DryRun() {
inv.DryRun = true
result, err := (executor.EchoRunner{}).Run(ctx, inv)
if err != nil {
return nil, err
}
return convertResult(result), nil
}
if a == nil || a.runner == nil {
return nil, fmt.Errorf("ToolCaller runner is not configured")
}
result, err := a.runner.Run(ctx, inv)
if err != nil {
return nil, err
@@ -44,25 +60,25 @@ func (a *toolCallerAdapter) CallTool(ctx context.Context, productID, toolName st
}
func (a *toolCallerAdapter) Format() string {
if a.flags != nil {
if a != nil && a.flags != nil {
return a.flags.Format
}
return "json"
}
func (a *toolCallerAdapter) DryRun() bool {
return a.flags != nil && a.flags.DryRun
return a != nil && a.flags != nil && a.flags.DryRun
}
func (a *toolCallerAdapter) Fields() string {
if a.flags != nil {
if a != nil && a.flags != nil {
return a.flags.Fields
}
return ""
}
func (a *toolCallerAdapter) JQ() string {
if a.flags != nil {
if a != nil && a.flags != nil {
return a.flags.JQ
}
return ""
+250
View File
@@ -0,0 +1,250 @@
package audit
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
func TestFileSinkEmit(t *testing.T) {
dir := t.TempDir()
writer, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
chain := NewChain(dir)
sink := NewFileSink(writer, chain, nil)
defer sink.Close()
evt := &Event{
Timestamp: time.Now(),
ExecutionID: "abc123",
Actor: Actor{UserID: "u1", CorpID: "c1"},
Product: "calendar",
Command: "list_events",
Endpoint: "https://api.example.com/mcp",
Result: "success",
DurationMs: 150,
CLIVersion: "1.0.47",
OS: "darwin",
Arch: "arm64",
}
if err := sink.Emit(evt); err != nil {
t.Fatal(err)
}
if evt.Hash == "" {
t.Error("expected hash to be set")
}
if evt.PrevHash != "" {
t.Error("first event should have empty prev_hash")
}
file, err := LatestAuditFile(dir)
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
var decoded Event
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded.ExecutionID != "abc123" {
t.Errorf("got execution_id=%s, want abc123", decoded.ExecutionID)
}
if decoded.Hash == "" {
t.Error("decoded hash should not be empty")
}
}
func TestChainIntegrity(t *testing.T) {
dir := t.TempDir()
writer, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
chain := NewChain(dir)
sink := NewFileSink(writer, chain, nil)
for i := 0; i < 5; i++ {
evt := &Event{
Timestamp: time.Now(),
ExecutionID: "exec-" + string(rune('a'+i)),
Actor: Actor{UserID: "u1", CorpID: "c1"},
Product: "test",
Command: "cmd",
Result: "success",
DurationMs: int64(i * 10),
CLIVersion: "1.0.0",
OS: "linux",
Arch: "amd64",
}
if err := sink.Emit(evt); err != nil {
t.Fatal(err)
}
}
sink.Close()
file, err := LatestAuditFile(dir)
if err != nil {
t.Fatal(err)
}
valid, brokenAt, err := VerifyFile(file)
if err != nil {
t.Fatalf("verify error: %v", err)
}
if !valid {
t.Errorf("expected valid chain, broken at line %d", brokenAt)
}
}
func TestChainDetectsTampering(t *testing.T) {
dir := t.TempDir()
writer, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
chain := NewChain(dir)
sink := NewFileSink(writer, chain, nil)
for i := 0; i < 3; i++ {
evt := &Event{
Timestamp: time.Now(),
ExecutionID: "exec-" + string(rune('0'+i)),
Actor: Actor{UserID: "u1", CorpID: "c1"},
Product: "test",
Command: "cmd",
Result: "success",
DurationMs: 100,
CLIVersion: "1.0.0",
OS: "linux",
Arch: "amd64",
}
if err := sink.Emit(evt); err != nil {
t.Fatal(err)
}
}
sink.Close()
file, err := LatestAuditFile(dir)
if err != nil {
t.Fatal(err)
}
// Tamper with the file: modify a character in the second line
data, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
// Find second newline and change a char after it
lines := splitLines(data)
if len(lines) < 2 {
t.Fatal("expected at least 2 lines")
}
// Corrupt the second line by changing first char of product
var evt2 map[string]any
json.Unmarshal([]byte(lines[1]), &evt2)
evt2["product"] = "tampered"
tampered, _ := json.Marshal(evt2)
lines[1] = string(tampered)
corrupted := []byte(lines[0] + "\n" + lines[1] + "\n" + lines[2] + "\n")
os.WriteFile(file, corrupted, 0o600)
valid, brokenAt, _ := VerifyFile(file)
if valid {
t.Error("expected invalid chain after tampering")
}
if brokenAt != 2 {
t.Errorf("expected break at line 2, got %d", brokenAt)
}
}
func TestRetention(t *testing.T) {
dir := t.TempDir()
// Create old files
oldDate := time.Now().AddDate(0, 0, -100).Format("20060102")
recentDate := time.Now().AddDate(0, 0, -10).Format("20060102")
os.WriteFile(filepath.Join(dir, "audit-"+oldDate+".jsonl"), []byte("old"), 0o600)
os.WriteFile(filepath.Join(dir, "audit-"+recentDate+".jsonl"), []byte("recent"), 0o600)
_, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
// Give async pruning a moment
time.Sleep(50 * time.Millisecond)
if _, err := os.Stat(filepath.Join(dir, "audit-"+oldDate+".jsonl")); !os.IsNotExist(err) {
t.Error("expected old file to be pruned")
}
if _, err := os.Stat(filepath.Join(dir, "audit-"+recentDate+".jsonl")); err != nil {
t.Error("recent file should still exist")
}
}
func TestRedactEvent(t *testing.T) {
evt := Event{
Actor: Actor{UserID: "uid123", Name: "张三", CorpID: "corp1", CorpName: "公司A"},
Product: "calendar",
Command: "list",
ParamsSummary: `{"date":"2026-01-01"}`,
Result: "success",
}
hashed := RedactEvent(evt, RedactHashed)
if hashed.Actor.Name == "张三" {
t.Error("name should be hashed")
}
if hashed.ParamsSummary != "" {
t.Error("params should be cleared in hashed mode")
}
if hashed.Actor.UserID != "uid123" {
t.Error("user_id should remain in hashed mode")
}
minimal := RedactEvent(evt, RedactMinimal)
if minimal.Actor.UserID == "uid123" {
t.Error("user_id should be hashed in minimal mode")
}
if minimal.Endpoint != "" {
t.Error("endpoint should be cleared in minimal mode")
}
}
func TestNopSink(t *testing.T) {
var s NopSink
if err := s.Emit(&Event{}); err != nil {
t.Error("NopSink.Emit should not error")
}
if err := s.Close(); err != nil {
t.Error("NopSink.Close should not error")
}
}
func splitLines(data []byte) []string {
var lines []string
start := 0
for i, b := range data {
if b == '\n' {
if i > start {
lines = append(lines, string(data[start:i]))
}
start = i + 1
}
}
if start < len(data) {
lines = append(lines, string(data[start:]))
}
return lines
}
+184
View File
@@ -0,0 +1,184 @@
package audit
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
)
// Chain computes the L1 sha256 tamper-evidence chain. It is deliberately
// stateless: every seal derives prev_hash from the last record already present
// in the current day's file rather than from an in-memory or sidecar cursor.
//
// This makes the chain correct in two cases the previous global-sidecar design
// broke:
// - cross-date: each day rotates to a fresh file whose first record chains
// from "", so every file is independently verifiable by VerifyFile.
// - cross-process: because the caller holds an exclusive inter-process lock on
// the file while sealing, the tail read reflects records written by any
// other dws process, so concurrent writers cannot fork the chain.
type Chain struct{}
// NewChain keeps the historical constructor signature. The directory argument
// is no longer needed because prev_hash is derived from the target file.
func NewChain(string) *Chain { return &Chain{} }
// SealFromFile reads the hash of the last record in f (the current day's audit
// file) and returns the prev_hash / hash pair for the event whose hash-free
// body is provided. The caller must hold the file lock.
func (c *Chain) SealFromFile(f *os.File, body []byte) (prevHash, hash string, err error) {
prevHash, err = lastRecordHash(f)
if err != nil {
return "", "", err
}
return prevHash, ComputeHash(prevHash, body), nil
}
// lastRecordHash returns the "hash" field of the final non-empty JSONL record in
// f, or "" when the file is empty. It reads only the tail of the file so cost
// does not grow with file size.
func lastRecordHash(f *os.File) (string, error) {
fi, err := f.Stat()
if err != nil {
return "", err
}
size := fi.Size()
if size == 0 {
return "", nil
}
const tailWindow = 64 * 1024
start := size - tailWindow
if start < 0 {
start = 0
}
buf := make([]byte, size-start)
if _, err := f.ReadAt(buf, start); err != nil && err != io.EOF {
return "", err
}
// Trim trailing newlines, then isolate the last line within the window.
end := len(buf)
for end > 0 && (buf[end-1] == '\n' || buf[end-1] == '\r') {
end--
}
if end == 0 {
return "", nil
}
lineStart := end
for lineStart > 0 && buf[lineStart-1] != '\n' {
lineStart--
}
last := buf[lineStart:end]
var rec struct {
Hash string `json:"hash"`
}
if err := json.Unmarshal(last, &rec); err != nil {
// The last record spilled past our tail window (pathologically large
// line). Fall back to a full scan for correctness.
if lineStart == 0 && start > 0 {
return lastRecordHashFullScan(f)
}
return "", fmt.Errorf("audit: parse last record: %w", err)
}
return rec.Hash, nil
}
func lastRecordHashFullScan(f *os.File) (string, error) {
if _, err := f.Seek(0, io.SeekStart); err != nil {
return "", err
}
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 8*1024*1024)
var last []byte
for scanner.Scan() {
if b := scanner.Bytes(); len(b) > 0 {
last = append(last[:0], b...)
}
}
if err := scanner.Err(); err != nil {
return "", err
}
if len(last) == 0 {
return "", nil
}
var rec struct {
Hash string `json:"hash"`
}
if err := json.Unmarshal(last, &rec); err != nil {
return "", fmt.Errorf("audit: parse last record: %w", err)
}
return rec.Hash, nil
}
func VerifyFile(path string) (valid bool, brokenAt int, err error) {
f, err := os.Open(path)
if err != nil {
return false, 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 8*1024*1024)
prevHash := ""
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
var evt struct {
PrevHash string `json:"prev_hash"`
Hash string `json:"hash"`
}
if err := json.Unmarshal(line, &evt); err != nil {
return false, lineNum, fmt.Errorf("line %d: invalid JSON: %w", lineNum, err)
}
if evt.PrevHash != prevHash {
return false, lineNum, fmt.Errorf("line %d: prev_hash mismatch", lineNum)
}
body := stripHashFields(line)
h := sha256.New()
h.Write([]byte(prevHash))
h.Write(body)
expected := hex.EncodeToString(h.Sum(nil))
if evt.Hash != expected {
return false, lineNum, fmt.Errorf("line %d: hash mismatch", lineNum)
}
prevHash = evt.Hash
}
if err := scanner.Err(); err != nil {
return false, lineNum, err
}
return true, 0, nil
}
func stripHashFields(line []byte) []byte {
var evt Event
if err := json.Unmarshal(line, &evt); err != nil {
return line
}
evt.PrevHash = ""
evt.Hash = ""
out, err := json.Marshal(evt)
if err != nil {
return line
}
return out
}
func ComputeHash(prevHash string, eventJSON []byte) string {
h := sha256.New()
h.Write([]byte(prevHash))
h.Write(eventJSON)
return hex.EncodeToString(h.Sum(nil))
}
+135
View File
@@ -0,0 +1,135 @@
package audit
import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
)
const (
EnvAudit = "DWS_AUDIT"
EnvAuditDir = "DWS_AUDIT_DIR"
EnvRetentionDays = "DWS_AUDIT_RETENTION_DAYS"
EnvForwardURL = "DWS_AUDIT_FORWARD_URL"
EnvForwardToken = "DWS_AUDIT_FORWARD_TOKEN"
EnvForwardRedact = "DWS_AUDIT_FORWARD_REDACT"
EnvAuditDebug = "DWS_AUDIT_DEBUG"
defaultRetentionDays = 90
auditSubdir = "audit"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: EnvAudit,
Category: configmeta.CategoryAudit,
Description: "操作审计日志开关(默认启用,设 0/false/off 关闭)",
DefaultValue: "启用",
Example: "0",
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvAuditDir,
Category: configmeta.CategoryAudit,
Description: "审计日志目录(默认 <configDir>/audit)",
Example: "/var/log/dws-audit",
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvRetentionDays,
Category: configmeta.CategoryAudit,
Description: "审计日志留存天数",
DefaultValue: "90",
Example: "180",
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvForwardURL,
Category: configmeta.CategoryAudit,
Description: "审计事件远端转发 URL(POST JSON)",
Example: "https://siem.example.com/audit",
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvForwardToken,
Category: configmeta.CategoryAudit,
Description: "远端转发 Bearer Token",
Sensitive: true,
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvForwardRedact,
Category: configmeta.CategoryAudit,
Description: "远端转发脱敏级别:none / hashed / minimal",
DefaultValue: "none",
Example: "hashed",
})
configmeta.Register(configmeta.ConfigItem{
Name: EnvAuditDebug,
Category: configmeta.CategoryAudit,
Description: "打印审计子系统初始化/写入/转发失败诊断到 stderr(设 1/true/on 开启)",
Example: "1",
})
}
// DebugEnabled reports whether audit-subsystem diagnostics should be surfaced to
// stderr. Failures are always eligible for the structured log; this gates the
// noisier stderr channel.
func DebugEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(EnvAuditDebug))) {
case "1", "true", "on", "yes", "y":
return true
}
return false
}
func IsEnabled() bool {
v := os.Getenv(EnvAudit)
if v == "" {
return true
}
switch strings.ToLower(v) {
case "0", "false", "off", "no", "n":
return false
}
return true
}
// BuildSink constructs the audit sink for configDir. It returns an error when
// the audit subsystem is enabled but cannot initialize (e.g. the log directory
// is not writable) so the caller can surface the failure instead of silently
// degrading. report receives non-fatal forwarder diagnostics; it may be nil.
func BuildSink(configDir string, report func(format string, args ...any)) (Sink, error) {
if !IsEnabled() {
return NopSink{}, nil
}
dir := os.Getenv(EnvAuditDir)
if dir == "" {
dir = filepath.Join(configDir, auditSubdir)
}
retention := defaultRetentionDays
if v := os.Getenv(EnvRetentionDays); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
retention = n
}
}
writer, err := NewDateRotatingWriter(dir, retention)
if err != nil {
return NopSink{}, err
}
chain := NewChain(dir)
var forwarder *HTTPForwarder
if fwdURL := os.Getenv(EnvForwardURL); fwdURL != "" {
token := os.Getenv(EnvForwardToken)
redact := RedactLevel(strings.ToLower(os.Getenv(EnvForwardRedact)))
if redact != RedactHashed && redact != RedactMinimal {
redact = RedactNone
}
forwarder = NewHTTPForwarder(fwdURL, token, redact, report)
}
return NewFileSink(writer, chain, forwarder), nil
}
+30
View File
@@ -0,0 +1,30 @@
package audit
import "time"
type Event struct {
Timestamp time.Time `json:"ts"`
ExecutionID string `json:"execution_id"`
AgentID string `json:"agent_id,omitempty"`
Actor Actor `json:"actor"`
Product string `json:"product"`
Command string `json:"command"`
Endpoint string `json:"endpoint"`
ParamsSummary string `json:"params_summary,omitempty"`
Result string `json:"result"`
ErrCategory string `json:"error_category,omitempty"`
ErrReason string `json:"error_reason,omitempty"`
DurationMs int64 `json:"duration_ms"`
CLIVersion string `json:"cli_version"`
OS string `json:"os"`
Arch string `json:"arch"`
PrevHash string `json:"prev_hash"`
Hash string `json:"hash"`
}
type Actor struct {
UserID string `json:"user_id"`
Name string `json:"name,omitempty"`
CorpID string `json:"corp_id"`
CorpName string `json:"corp_name,omitempty"`
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package audit
import (
"os"
"syscall"
)
// lockFile takes a non-blocking exclusive advisory lock; returns an error when
// another process holds it so the caller can retry with a timeout.
func lockFile(f *os.File) error {
return syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
}
func unlockFile(f *os.File) {
_ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package audit
import (
"os"
"unsafe"
"golang.org/x/sys/windows"
)
const lockfileExclusiveLock = 0x00000002
// lockFile takes a non-blocking exclusive lock on the first byte range; returns
// an error when another process holds it so the caller can retry with a timeout.
func lockFile(f *os.File) error {
ol := new(windows.Overlapped)
return windows.LockFileEx(
windows.Handle(f.Fd()),
lockfileExclusiveLock|windows.LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
(*windows.Overlapped)(unsafe.Pointer(ol)),
)
}
func unlockFile(f *os.File) {
ol := new(windows.Overlapped)
_ = windows.UnlockFileEx(
windows.Handle(f.Fd()),
0,
1,
0,
(*windows.Overlapped)(unsafe.Pointer(ol)),
)
}
+109
View File
@@ -0,0 +1,109 @@
package audit
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
type HTTPForwarder struct {
url string
token string
redact RedactLevel
client *http.Client
wg sync.WaitGroup
report func(format string, args ...any)
timeout time.Duration
}
func NewHTTPForwarder(url, token string, redact RedactLevel, report func(string, ...any)) *HTTPForwarder {
if report == nil {
report = func(string, ...any) {}
}
return &HTTPForwarder{
url: url,
token: token,
redact: redact,
client: &http.Client{Timeout: 3 * time.Second},
report: report,
timeout: 3 * time.Second,
}
}
// Forward dispatches the event asynchronously while tracking the goroutine so
// Close can wait for delivery instead of the CLI dropping it on exit.
func (f *HTTPForwarder) Forward(evt Event) {
f.wg.Add(1)
go func() {
defer f.wg.Done()
f.send(evt)
}()
}
// Close waits for in-flight forwards to finish, bounded by ctx (and, if ctx has
// no deadline, by a small internal timeout) so shutdown never blocks forever.
func (f *HTTPForwarder) Close(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, f.timeout+2*time.Second)
defer cancel()
}
done := make(chan struct{})
go func() {
f.wg.Wait()
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
f.report("forward flush timed out: %v", ctx.Err())
return fmt.Errorf("audit: forward flush timed out: %w", ctx.Err())
}
}
func (f *HTTPForwarder) send(evt Event) {
var body []byte
var err error
if f.redact != RedactNone {
body, err = RedactEventJSON(evt, f.redact)
} else {
body, err = json.Marshal(evt)
}
if err != nil {
f.report("forward marshal failed: %v", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), f.timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, f.url, bytes.NewReader(body))
if err != nil {
f.report("forward build request failed: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
if f.token != "" {
req.Header.Set("Authorization", "Bearer "+f.token)
}
resp, err := f.client.Do(req)
if err != nil {
f.report("forward request failed: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
f.report("forward rejected: status %d", resp.StatusCode)
}
}
+47
View File
@@ -0,0 +1,47 @@
package audit
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
)
type RedactLevel string
const (
RedactNone RedactLevel = "none"
RedactHashed RedactLevel = "hashed"
RedactMinimal RedactLevel = "minimal"
)
func RedactEvent(evt Event, level RedactLevel) Event {
switch level {
case RedactHashed:
if evt.Actor.Name != "" {
evt.Actor.Name = hashString(evt.Actor.Name)
}
if evt.Actor.CorpName != "" {
evt.Actor.CorpName = hashString(evt.Actor.CorpName)
}
evt.ParamsSummary = ""
case RedactMinimal:
evt.Actor = Actor{UserID: hashString(evt.Actor.UserID), CorpID: hashString(evt.Actor.CorpID)}
evt.ParamsSummary = ""
evt.Endpoint = ""
evt.ErrReason = ""
evt.AgentID = ""
evt.PrevHash = ""
evt.Hash = ""
}
return evt
}
func RedactEventJSON(evt Event, level RedactLevel) ([]byte, error) {
redacted := RedactEvent(evt, level)
return json.Marshal(redacted)
}
func hashString(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:8])
}
+225
View File
@@ -0,0 +1,225 @@
package audit
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
)
// writeEvent seals and appends one event through the writer+chain, mirroring
// FileSink.Emit's locking discipline, so tests exercise the real tail-derived
// hash chain path.
func writeEvent(t *testing.T, w *DateRotatingWriter, chain *Chain, evt *Event) {
t.Helper()
body, err := marshalWithoutHash(evt)
if err != nil {
t.Fatalf("marshal: %v", err)
}
f, release, err := w.beginAppend()
if err != nil {
t.Fatalf("beginAppend: %v", err)
}
prev, hash, err := chain.SealFromFile(f, body)
if err != nil {
release()
t.Fatalf("seal: %v", err)
}
evt.PrevHash, evt.Hash = prev, hash
line, err := json.Marshal(evt)
if err != nil {
release()
t.Fatalf("marshal final: %v", err)
}
if _, err := f.Write(append(line, '\n')); err != nil {
release()
t.Fatalf("write: %v", err)
}
release()
}
func sampleEvent(id string) *Event {
return &Event{
Timestamp: time.Now(),
ExecutionID: id,
Actor: Actor{UserID: "u1", CorpID: "c1"},
Product: "calendar",
Command: "event_list",
Result: "success",
DurationMs: 10,
CLIVersion: "1.0.0",
OS: "darwin",
Arch: "arm64",
}
}
// TestCrossDateChainIndependentPerFile verifies each day's file starts a fresh
// chain (prev_hash="") and verifies independently — the bug the removed global
// .chain sidecar introduced.
func TestCrossDateChainIndependentPerFile(t *testing.T) {
dir := t.TempDir()
chain := NewChain(dir)
// Simulate two calendar days by writing files directly with the same chain
// semantics: each file's first record must chain from "".
for _, day := range []string{"20260101", "20260102"} {
path := filepath.Join(dir, "audit-"+day+".jsonl")
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 3; i++ {
evt := sampleEvent(fmt.Sprintf("%s-%d", day, i))
body, _ := marshalWithoutHash(evt)
prev, hash, err := chain.SealFromFile(f, body)
if err != nil {
t.Fatal(err)
}
evt.PrevHash, evt.Hash = prev, hash
line, _ := json.Marshal(evt)
if _, err := f.Write(append(line, '\n')); err != nil {
t.Fatal(err)
}
}
f.Close()
valid, brokenAt, err := VerifyFile(path)
if err != nil {
t.Fatalf("verify %s: %v", day, err)
}
if !valid {
t.Fatalf("file %s chain broken at line %d", day, brokenAt)
}
}
}
// TestCrossProcessChainSharedFile simulates two independent writers (as two
// processes would) appending to the same day's file. Because each seal derives
// prev_hash from the file tail under the inter-process lock, the resulting chain
// must remain valid with no fork.
func TestCrossProcessChainSharedFile(t *testing.T) {
dir := t.TempDir()
w1, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
defer w1.Close()
w2, err := NewDateRotatingWriter(dir, 90)
if err != nil {
t.Fatal(err)
}
defer w2.Close()
chain := NewChain(dir)
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
w := w1
if i%2 == 1 {
w = w2
}
go func(w *DateRotatingWriter, i int) {
defer wg.Done()
writeEvent(t, w, chain, sampleEvent(fmt.Sprintf("exec-%d", i)))
}(w, i)
}
wg.Wait()
file, err := LatestAuditFile(dir)
if err != nil {
t.Fatal(err)
}
valid, brokenAt, err := VerifyFile(file)
if err != nil {
t.Fatalf("verify: %v", err)
}
if !valid {
t.Fatalf("cross-process chain broken at line %d", brokenAt)
}
}
// TestForwarderCloseWaitsForDelivery ensures Close blocks until every async
// forward has been delivered to the remote endpoint.
func TestForwarderCloseWaitsForDelivery(t *testing.T) {
var received int64
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-release // hold the handler so delivery is still in flight at Close time
atomic.AddInt64(&received, 1)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
fwd := NewHTTPForwarder(srv.URL, "", RedactNone, nil)
const n = 5
for i := 0; i < n; i++ {
fwd.Forward(*sampleEvent(fmt.Sprintf("e-%d", i)))
}
// Nothing delivered yet because handlers are blocked.
if got := atomic.LoadInt64(&received); got != 0 {
t.Fatalf("expected 0 delivered before release, got %d", got)
}
close(release)
if err := fwd.Close(context.Background()); err != nil {
t.Fatalf("Close returned error: %v", err)
}
if got := atomic.LoadInt64(&received); got != n {
t.Fatalf("expected %d delivered after Close, got %d", n, got)
}
}
// TestForwarderCloseTimeoutReports verifies Close honors the ctx deadline and
// reports instead of blocking forever when the endpoint never responds.
func TestForwarderCloseTimeoutReports(t *testing.T) {
block := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-block
}))
// Defers run LIFO: close(block) first unblocks the handler so srv.Close can
// join its connection goroutine without deadlocking.
defer srv.Close()
defer close(block)
var reported int64
report := func(string, ...any) { atomic.AddInt64(&reported, 1) }
fwd := NewHTTPForwarder(srv.URL, "", RedactNone, report)
fwd.timeout = 10 * time.Second // keep the in-flight send alive past ctx
fwd.Forward(*sampleEvent("stuck"))
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
if err := fwd.Close(ctx); err == nil {
t.Fatal("expected timeout error from Close")
}
if atomic.LoadInt64(&reported) == 0 {
t.Fatal("expected Close timeout to be reported")
}
}
// TestBuildSinkInitFailureObservable verifies BuildSink surfaces an error (and
// the caller can fall back) when the audit directory cannot be created.
func TestBuildSinkInitFailureObservable(t *testing.T) {
dir := t.TempDir()
// Make a file where the audit subdir is expected so MkdirAll fails.
clash := filepath.Join(dir, "audit")
if err := os.WriteFile(clash, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(EnvAuditDir, filepath.Join(clash, "sub"))
var reported int64
_, err := BuildSink(dir, func(string, ...any) { atomic.AddInt64(&reported, 1) })
if err == nil {
t.Fatal("expected BuildSink to fail when audit dir is unusable")
}
}
+182
View File
@@ -0,0 +1,182 @@
package audit
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
const (
auditLockFile = ".audit.lock"
auditLockTimeout = 3 * time.Second
auditLockRetry = 20 * time.Millisecond
)
type DateRotatingWriter struct {
mu sync.Mutex
dir string
curDate string
file *os.File
lock *os.File
retention int
}
func NewDateRotatingWriter(dir string, retentionDays int) (*DateRotatingWriter, error) {
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, fmt.Errorf("audit: create dir: %w", err)
}
lock, err := os.OpenFile(filepath.Join(dir, auditLockFile), os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return nil, fmt.Errorf("audit: open lock file: %w", err)
}
w := &DateRotatingWriter{
dir: dir,
retention: retentionDays,
lock: lock,
}
go w.pruneOldFiles()
return w, nil
}
// beginAppend serializes writers within this process (mu) and across processes
// (flock), rotates to today's file, and returns the open handle plus a release
// func that unlocks in reverse order. The file is opened O_RDWR|O_APPEND so the
// chain can read the tail while every write still lands atomically at EOF even
// when another dws process appends concurrently.
func (w *DateRotatingWriter) beginAppend() (*os.File, func(), error) {
w.mu.Lock()
if err := w.acquireLock(); err != nil {
w.mu.Unlock()
return nil, nil, err
}
today := time.Now().Format("20060102")
if today != w.curDate || w.file == nil {
if w.file != nil {
_ = w.file.Close()
w.file = nil
}
path := filepath.Join(w.dir, fmt.Sprintf("audit-%s.jsonl", today))
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
if err != nil {
unlockFile(w.lock)
w.mu.Unlock()
return nil, nil, fmt.Errorf("audit: open file: %w", err)
}
w.file = f
w.curDate = today
}
release := func() {
unlockFile(w.lock)
w.mu.Unlock()
}
return w.file, release, nil
}
func (w *DateRotatingWriter) acquireLock() error {
deadline := time.Now().Add(auditLockTimeout)
for {
if err := lockFile(w.lock); err == nil {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("audit: timeout acquiring file lock after %v (another dws process may be writing)", auditLockTimeout)
}
time.Sleep(auditLockRetry)
}
}
func (w *DateRotatingWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
var firstErr error
if w.file != nil {
if err := w.file.Close(); err != nil {
firstErr = err
}
w.file = nil
}
if w.lock != nil {
if err := w.lock.Close(); err != nil && firstErr == nil {
firstErr = err
}
w.lock = nil
}
return firstErr
}
func (w *DateRotatingWriter) pruneOldFiles() {
if w.retention <= 0 {
return
}
entries, err := os.ReadDir(w.dir)
if err != nil {
return
}
cutoff := time.Now().AddDate(0, 0, -w.retention).Format("20060102")
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, "audit-") || !strings.HasSuffix(name, ".jsonl") {
continue
}
dateStr := strings.TrimPrefix(name, "audit-")
dateStr = strings.TrimSuffix(dateStr, ".jsonl")
if len(dateStr) != 8 {
continue
}
if dateStr < cutoff {
_ = os.Remove(filepath.Join(w.dir, name))
}
}
}
func (w *DateRotatingWriter) Dir() string {
return w.dir
}
func LatestAuditFile(dir string) (string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", err
}
var files []string
for _, e := range entries {
if strings.HasPrefix(e.Name(), "audit-") && strings.HasSuffix(e.Name(), ".jsonl") {
files = append(files, e.Name())
}
}
if len(files) == 0 {
return "", fmt.Errorf("no audit files found in %s", dir)
}
sort.Strings(files)
return filepath.Join(dir, files[len(files)-1]), nil
}
func AuditFilesInRange(dir, since, until string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var files []string
for _, e := range entries {
name := e.Name()
if !strings.HasPrefix(name, "audit-") || !strings.HasSuffix(name, ".jsonl") {
continue
}
dateStr := strings.TrimPrefix(name, "audit-")
dateStr = strings.TrimSuffix(dateStr, ".jsonl")
if len(dateStr) != 8 {
continue
}
if (since == "" || dateStr >= since) && (until == "" || dateStr <= until) {
files = append(files, filepath.Join(dir, name))
}
}
sort.Strings(files)
return files, nil
}
+109
View File
@@ -0,0 +1,109 @@
package audit
import (
"context"
"encoding/json"
"fmt"
"time"
)
type Sink interface {
Emit(event *Event) error
Close() error
}
type NopSink struct{}
func (NopSink) Emit(*Event) error { return nil }
func (NopSink) Close() error { return nil }
type FileSink struct {
writer *DateRotatingWriter
chain *Chain
forwarder *HTTPForwarder
}
func NewFileSink(writer *DateRotatingWriter, chain *Chain, forwarder *HTTPForwarder) *FileSink {
return &FileSink{
writer: writer,
chain: chain,
forwarder: forwarder,
}
}
func (s *FileSink) Emit(evt *Event) error {
body, err := marshalWithoutHash(evt)
if err != nil {
return fmt.Errorf("audit: marshal event: %w", err)
}
f, release, err := s.writer.beginAppend()
if err != nil {
return fmt.Errorf("audit: acquire writer: %w", err)
}
// Derive prev_hash from the file tail, seal, and append — all under the
// writer's process + inter-process lock so the chain cannot fork.
prevHash, hash, err := s.chain.SealFromFile(f, body)
if err != nil {
release()
return fmt.Errorf("audit: seal event: %w", err)
}
evt.PrevHash = prevHash
evt.Hash = hash
line, err := json.Marshal(evt)
if err != nil {
release()
return fmt.Errorf("audit: marshal final event: %w", err)
}
line = append(line, '\n')
if _, err := f.Write(line); err != nil {
release()
return fmt.Errorf("audit: write event: %w", err)
}
release()
if s.forwarder != nil {
s.forwarder.Forward(*evt)
}
return nil
}
// Close flushes in-flight remote forwards (bounded) before closing the writer,
// so events are not silently dropped when the CLI process exits right after
// emitting.
func (s *FileSink) Close() error {
var forwardErr error
if s.forwarder != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
forwardErr = s.forwarder.Close(ctx)
cancel()
}
if err := s.writer.Close(); err != nil {
return err
}
return forwardErr
}
func marshalWithoutHash(evt *Event) ([]byte, error) {
saved := Event{
Timestamp: evt.Timestamp,
ExecutionID: evt.ExecutionID,
AgentID: evt.AgentID,
Actor: evt.Actor,
Product: evt.Product,
Command: evt.Command,
Endpoint: evt.Endpoint,
ParamsSummary: evt.ParamsSummary,
Result: evt.Result,
ErrCategory: evt.ErrCategory,
ErrReason: evt.ErrReason,
DurationMs: evt.DurationMs,
CLIVersion: evt.CLIVersion,
OS: evt.OS,
Arch: evt.Arch,
}
return json.Marshal(saved)
}
+39 -41
View File
@@ -15,13 +15,13 @@ package cli
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline"
"github.com/spf13/cobra"
)
@@ -67,61 +67,59 @@ func NewMCPCommand(_ context.Context, _ CatalogLoader, _ executor.Runner, _ *pip
return cmd
}
// NewSchemaCommand returns a stub schema command since the canonical
// catalog discovery has been removed.
func NewSchemaCommand(_ CatalogLoader, helperTools HelperToolFetcher) *cobra.Command {
// NewSchemaCommand serves the versioned embedded typed contract. A malformed
// release snapshot fails closed; falling back to the live Cobra tree would
// hide a broken delivery artifact and reintroduce a second Schema data path.
func NewSchemaCommand(_ CatalogLoader) *cobra.Command {
cmd := &cobra.Command{
Use: "schema [path]",
Short: "查看有限的本地 Schema(静态端点模式)",
Long: `查看有限的本地 Schema 元数据。
Short: "渐进查看命令 Schema (产品 / 分组 / 工具参数)",
Long: `查看当前可运行命令的 Schema 元数据。
服务发现和动态 schema 已下线。静态端点模式下,schema 覆盖两类命令:
1. helper-only 子树(如 dev):CONTENT 从其绑定的 MCP 服务实时取,source 为 mcp:<server>;
2. 登记的本地命令(如 event):从二进制注册的 cobra flag 合成,source 为 cobra。
其余普通产品命令和 flag 仍以当前二进制的 --help 为准。`,
不带参数时列出产品和工具数量;传产品或分组路径逐层展开;传具体工具路径输出扁平参数 Schema(对齐 GWS:parameters 内联 required,键为 CLI flag)。--all 输出全部工具的完整 leaf Schema(包括参数和约束,用于审计/CI)。--compact 去除 provenance / debug 字段,仅保留 Agent 选参所需信息(适合 Agent 上下文)。helper、MCP 与本地 Cobra 命令均须先进入 reviewed Registry,并从同一内嵌 ToolSpec 投影;查询不执行服务发现或临时合成第二份 Schema。`,
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
all, _ := cmd.Flags().GetBool("all")
compact, _ := cmd.Flags().GetBool("compact")
cliPath, _ := cmd.Flags().GetString("cli-path")
cliPath = strings.TrimSpace(cliPath)
if cliPath != "" && len(args) > 0 {
return apperrors.NewValidation("--cli-path and positional argument are mutually exclusive")
}
if len(args) == 1 && strings.EqualFold(strings.TrimSpace(args[0]), "list") {
args = nil
}
if all && (cliPath != "" || len(args) > 0) {
return apperrors.NewValidation("--all cannot be combined with a schema path")
}
if cliPath != "" {
if len(args) > 0 {
return apperrors.NewValidation("--cli-path and positional argument are mutually exclusive")
}
args = []string{cliPath}
}
// Helper-only subtrees: schema CONTENT fetched live from the MCP server.
if len(args) > 0 && helperTools != nil {
payload, ok, err := renderHelperSchema(cmd.Context(), cmd.Root(), args[0], helperTools)
if err != nil {
return err
}
if ok {
data, _ := json.MarshalIndent(payload, "", " ")
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}
if err := embeddedSchemaCatalogError(); err != nil {
return fmt.Errorf("load embedded typed Schema registry: %w", err)
}
// Registered local subtrees (event, …): schema synthesized from cobra flags.
if len(args) > 0 {
payload, ok, err := renderCobraSchema(cmd.Root(), args[0])
if err != nil {
return err
}
if ok {
data, _ := json.MarshalIndent(payload, "", " ")
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}
var payload map[string]any
var err error
if all {
payload, err = embeddedSchemaAllPayload()
} else if len(args) == 0 {
payload, err = embeddedSchemaOverviewPayload()
} else {
payload, err = embeddedSchemaPayload(args)
}
fmt.Fprintln(cmd.OutOrStdout(), `{"kind":"schema","count":0,"products":[],"note":"static endpoint mode"}`)
return nil
if err != nil {
return err
}
if compact {
payload = stripSchemaPayloadCompact(payload)
}
return output.WriteFiltered(cmd.OutOrStdout(), output.ResolveFormat(cmd, output.FormatJSON), payload, output.ResolveFields(cmd), output.ResolveJQ(cmd))
},
}
cmd.Flags().String("cli-path", "", "按 CLI 命令路径查询 (等同于位置参数,便于脚本使用无需转义)")
cmd.Flags().Bool("all", false, "输出全部工具的完整 leaf Schema(包括参数和约束,用于审计/CI)")
cmd.Flags().Bool("compact", false, "去除 provenance/debug 字段,仅保留 Agent 选参所需信息")
cmd.Flags().String("cli-path", "", "按 CLI 命令路径查询")
return cmd
}
-204
View File
@@ -1,204 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// cobraSchemaRoots are top-level command names whose subtrees answer `dws schema`
// by SYNTHESIZING the machine-readable input schema from their cobra flags. This
// is the local-command counterpart to helperSchemaRoots: helper subtrees (dev)
// fetch CONTENT live from an MCP server, whereas these commands have no MCP
// backing so the schema is built from the flags the binary actually registered.
//
// The output shape is the same flat object helper leaves emit — {description,
// path, source, parameters{<flag>:{type,description,required,default?}}} — so an
// agent gets one consistent schema format no matter the source; `source` is
// "cobra" to mark it synthesized from flags (vs "mcp:<server>"). event is the
// first consumer; register more command trees here as they adopt the contract.
var cobraSchemaRoots = map[string]bool{"event": true}
// renderCobraSchema builds the `dws schema` payload for command subtrees listed
// in cobraSchemaRoots. Mirrors renderHelperSchema's routing: returns
// (payload, true) when the path targets a registered subtree so the caller
// skips the static-mode fallback; (nil, false) otherwise.
//
// A runnable leaf renders the flat parameter object synthesized from its flags
// (plus positional arguments parsed from its Use line). A group/root renders the
// same browse listing helper groups use.
func renderCobraSchema(root *cobra.Command, rawPath string) (map[string]any, bool, error) {
if root == nil {
return nil, false, nil
}
tokens := splitSchemaPathTokens(rawPath)
if len(tokens) == 0 || !cobraSchemaRoots[tokens[0]] {
return nil, false, nil
}
target, rest, err := root.Find(tokens)
if err != nil || target == nil {
target = root
rest = tokens[1:]
}
// Any non-flag leftover token means an unknown subcommand — surface it with
// the closest group's children, same as renderHelperSchema.
if unknown := firstNonFlag(rest); unknown != "" {
return map[string]any{
"path": rawPath,
"error": "unknown subcommand \"" + unknown + "\" under \"" + helperCommandPath(target) + "\"",
"available": helperSubcommands(target),
}, true, nil
}
if target.Runnable() && !target.HasAvailableSubCommands() {
return cobraLeafSchema(target), true, nil
}
return map[string]any{
"path": helperCommandPath(target),
"commands": helperSubcommands(target),
}, true, nil
}
// cobraLeafSchema renders one runnable command as the flat schema object,
// synthesizing parameters from its flags and (when present) positional
// arguments from its Use line.
func cobraLeafSchema(cmd *cobra.Command) map[string]any {
out := map[string]any{
"description": strings.TrimSpace(cmd.Short),
"path": helperCommandPath(cmd),
"source": "cobra",
"parameters": cobraFlatParameters(cmd),
}
if args := cobraPositionalArgs(cmd); len(args) > 0 {
out["arguments"] = args
}
return out
}
// cobraFlatParameters projects a command's LOCAL flags into the flat
// per-parameter object. Local (non-inherited) flags are the command-specific
// inputs; global persistent flags inherited from the root (--profile, --verbose,
// --jq, …) are intentionally excluded so the schema describes THIS command, not
// the whole CLI. Hidden internal flags are skipped. Each entry is
// {type, description, required, default?} with type mapped to a JSON-type
// string, required read from cobra's required-flag annotation, and default only
// when the flag has a meaningful (non-zero) default.
func cobraFlatParameters(cmd *cobra.Command) map[string]any {
params := map[string]any{}
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
if f.Hidden {
return
}
entry := map[string]any{
"type": pflagJSONType(f),
"description": strings.TrimSpace(f.Usage),
"required": flagIsRequired(f),
}
if def, ok := meaningfulDefault(f); ok {
entry["default"] = def
}
params[f.Name] = entry
})
return params
}
// cobraPositionalArgs parses a command's Use line into structured positional
// arguments. Cobra has no typed metadata for positionals, so the Use string
// ("consume [event_key]", "stop [subscribe_id]") is the source of truth:
// tokens after the command name are positional slots. <name> is required,
// [name] is optional, a trailing "..." marks it variadic. Returns nil when the
// command declares no positionals (e.g. flag-only commands), so the leaf object
// simply omits the "arguments" field.
func cobraPositionalArgs(cmd *cobra.Command) []map[string]any {
fields := strings.Fields(cmd.Use)
if len(fields) <= 1 {
return nil
}
out := []map[string]any{}
for _, tok := range fields[1:] {
variadic := strings.Contains(tok, "...")
required := strings.HasPrefix(tok, "<")
name := strings.Trim(tok, "[]<>.")
if name == "" {
continue
}
arg := map[string]any{
"name": name,
"required": required,
}
if variadic {
arg["variadic"] = true
}
out = append(out, arg)
}
if len(out) == 0 {
return nil
}
return out
}
// pflagJSONType maps a pflag value type to a JSON-type string. Duration is
// expressed as "string" because it is entered as a CLI string ("10m"); slice
// and array types collapse to "array"; the numeric families collapse to
// "integer"/"number". Unknown types default to "string" so the contract is
// always populated.
func pflagJSONType(f *pflag.Flag) string {
switch f.Value.Type() {
case "bool":
return "boolean"
case "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64", "count":
return "integer"
case "float32", "float64":
return "number"
case "stringSlice", "stringArray", "intSlice", "int32Slice", "int64Slice",
"uintSlice", "float32Slice", "float64Slice", "boolSlice", "durationSlice":
return "array"
default:
// string, duration, ip, and any custom Value type read as a string.
return "string"
}
}
// flagIsRequired reports whether cobra.MarkFlagRequired was applied to the flag
// (it records the requirement in the flag's annotations under
// cobra.BashCompOneRequiredFlag). Event's conditionally-required inputs
// (--user / --group depend on the event key) are enforced at runtime, not via
// this annotation, so they read as required:false here — the dependency is
// documented in the command help, not the flag metadata.
func flagIsRequired(f *pflag.Flag) bool {
if f.Annotations == nil {
return false
}
vals, ok := f.Annotations[cobra.BashCompOneRequiredFlag]
return ok && len(vals) == 1 && vals[0] == "true"
}
// meaningfulDefault returns a flag's default only when it is a real value, not
// the zero/unset sentinel ("", "0", "0s", "false", "[]"). A zero default means
// "no default — the value comes from you", so omitting it matches how the helper
// renderer omits absent MCP defaults and keeps the schema free of noise.
func meaningfulDefault(f *pflag.Flag) (string, bool) {
def := strings.TrimSpace(f.DefValue)
switch def {
case "", "0", "0s", "false", "[]", "{}":
return "", false
default:
return def, true
}
}
-227
View File
@@ -1,227 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"github.com/spf13/cobra"
)
// buildEventTestTree mirrors the shape of the real `dws event` subtree closely
// enough to exercise the cobra-flag schema renderer: a group with a runnable
// leaf that carries a positional arg, typed flags (string/int/duration/bool),
// a required flag, a hidden internal flag, and a defaulted flag.
func buildEventTestTree() *cobra.Command {
root := &cobra.Command{Use: "dws"}
// A global persistent flag inherited by every command — must NOT appear in a
// command's synthesized parameters (it describes the CLI, not the command).
root.PersistentFlags().String("profile", "", "组织 profile")
consume := &cobra.Command{
Use: "consume [event_key]",
Short: "订阅事件流并输出到 stdout",
Args: cobra.MaximumNArgs(1),
Run: func(*cobra.Command, []string) {},
}
f := consume.Flags()
f.StringP("format", "f", "ndjson", "输出格式")
f.String("user", "", "单聊对端 userId")
f.String("group", "", "群 openConversationId")
f.Int("max-events", 0, "收到 N 条后退出")
f.Duration("duration", 0, "运行时长上限")
f.Bool("ephemeral", false, "退出时强制退订")
f.String("subscribe-id", "", "复用已有订阅")
f.String("client-id", "", "内部:覆盖凭证解析")
_ = f.MarkHidden("client-id")
// A flag marked required via cobra — must read required:true.
f.String("token", "", "必填令牌")
_ = consume.MarkFlagRequired("token")
stop := &cobra.Command{
Use: "stop [subscribe_id]",
Short: "取消订阅",
Run: func(*cobra.Command, []string) {},
}
stop.Flags().Bool("all", false, "取消全部")
event := &cobra.Command{Use: "event", Short: "个人消息事件"}
// A hidden internal subcommand — must not appear in the browse listing.
bus := &cobra.Command{Use: "_bus", Short: "内部 bus", Hidden: true, Run: func(*cobra.Command, []string) {}}
event.AddCommand(consume, stop, bus)
root.AddCommand(event)
return root
}
func TestRenderCobraSchema_LeafFlatShape(t *testing.T) {
root := buildEventTestTree()
payload, ok, err := renderCobraSchema(root, "event consume")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected cobra renderer to claim the event path")
}
if payload["description"] != "订阅事件流并输出到 stdout" {
t.Fatalf("description = %v", payload["description"])
}
if payload["path"] != "event consume" {
t.Fatalf("path = %v", payload["path"])
}
if payload["source"] != "cobra" {
t.Fatalf("source = %v, want cobra", payload["source"])
}
params, _ := payload["parameters"].(map[string]any)
if params == nil {
t.Fatalf("no parameters: %#v", payload)
}
// Inherited global flag must be excluded.
if _, present := params["profile"]; present {
t.Fatal("inherited --profile must not appear in a command's parameters")
}
// Hidden internal flag must be excluded.
if _, present := params["client-id"]; present {
t.Fatal("hidden --client-id must not appear")
}
// Type mapping.
if got := paramField(t, params, "user", "type"); got != "string" {
t.Errorf("user type = %v, want string", got)
}
if got := paramField(t, params, "max-events", "type"); got != "integer" {
t.Errorf("max-events type = %v, want integer", got)
}
if got := paramField(t, params, "duration", "type"); got != "string" {
t.Errorf("duration type = %v, want string (CLI string like 10m)", got)
}
if got := paramField(t, params, "ephemeral", "type"); got != "boolean" {
t.Errorf("ephemeral type = %v, want boolean", got)
}
// Meaningful default is surfaced; zero defaults are omitted.
if got := paramField(t, params, "format", "default"); got != "ndjson" {
t.Errorf("format default = %v, want ndjson", got)
}
if _, hasDefault := params["max-events"].(map[string]any)["default"]; hasDefault {
t.Error("max-events has a zero default (0) — must be omitted")
}
if _, hasDefault := params["ephemeral"].(map[string]any)["default"]; hasDefault {
t.Error("ephemeral has a zero default (false) — must be omitted")
}
if _, hasDefault := params["duration"].(map[string]any)["default"]; hasDefault {
t.Error("duration has a zero default (0s) — must be omitted")
}
// Required annotation is honored; unmarked flags read required:false.
if got := paramField(t, params, "token", "required"); got != true {
t.Errorf("token required = %v, want true", got)
}
if got := paramField(t, params, "user", "required"); got != false {
t.Errorf("user required = %v, want false", got)
}
}
func TestRenderCobraSchema_PositionalArguments(t *testing.T) {
root := buildEventTestTree()
payload, _, err := renderCobraSchema(root, "event.consume")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
args, _ := payload["arguments"].([]map[string]any)
if len(args) != 1 {
t.Fatalf("arguments = %#v, want 1 positional", payload["arguments"])
}
if args[0]["name"] != "event_key" {
t.Errorf("arg name = %v, want event_key", args[0]["name"])
}
// [event_key] is optional syntax → required:false.
if args[0]["required"] != false {
t.Errorf("arg required = %v, want false", args[0]["required"])
}
}
func TestRenderCobraSchema_DotAndSpacePathEquivalent(t *testing.T) {
root := buildEventTestTree()
dotted, _, _ := renderCobraSchema(root, "event.consume")
spaced, _, _ := renderCobraSchema(root, "event consume")
if dotted["path"] != spaced["path"] || dotted["path"] != "event consume" {
t.Fatalf("dot/space forms diverged: %v vs %v", dotted["path"], spaced["path"])
}
}
func TestRenderCobraSchema_GroupBrowse(t *testing.T) {
root := buildEventTestTree()
payload, ok, err := renderCobraSchema(root, "event")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
cmds, _ := payload["commands"].([]map[string]any)
// consume + stop; hidden _bus excluded.
if len(cmds) != 2 {
t.Fatalf("commands = %#v, want 2 (hidden _bus excluded)", cmds)
}
for _, c := range cmds {
if c["cli_path"] == "event _bus" {
t.Fatal("hidden _bus must not be listed")
}
}
}
func TestRenderCobraSchema_UnknownSubcommand(t *testing.T) {
root := buildEventTestTree()
payload, ok, err := renderCobraSchema(root, "event nope")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
if payload["error"] == nil {
t.Fatalf("expected unknown-subcommand error, got %#v", payload)
}
if avail, _ := payload["available"].([]map[string]any); len(avail) == 0 {
t.Fatal("expected available subcommands listed")
}
}
func TestRenderCobraSchema_NonRegisteredPathDeclined(t *testing.T) {
root := buildEventTestTree()
if _, ok, _ := renderCobraSchema(root, "dev app create"); ok {
t.Fatal("non-registered path must not be claimed by the cobra renderer")
}
if _, ok, _ := renderCobraSchema(root, "ding.message.send"); ok {
t.Fatal("non-registered path must not be claimed")
}
}
// paramField fetches params[<name>][<field>], failing the test if the param is
// absent.
func paramField(t *testing.T, params map[string]any, name, field string) any {
t.Helper()
p, _ := params[name].(map[string]any)
if p == nil {
t.Fatalf("missing param %q in %#v", name, params)
}
return p[field]
}
-298
View File
@@ -1,298 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"fmt"
"sort"
"strings"
"unicode"
"github.com/spf13/cobra"
)
// helperSchemaRoots are top-level command names whose subtrees are helper-only
// (hard-coded cobra commands, not in the discovery catalog). `dws schema` still
// answers for them, but unlike discovery products the schema CONTENT is fetched
// LIVE from the helper's pinned MCP server (op-app) and rendered in the flat
// gws-aligned shape — never synthesized from local cobra flags, never
// hardcoded. The mapping from a leaf command to its MCP tool comes from the
// `mcp-tool` cobra annotation set in internal/helpers/devapp.go.
var helperSchemaRoots = map[string]bool{"dev": true}
// HelperToolSchema is the live op-app tool schema the renderer needs: the raw
// MCP description plus the inputSchema's properties/required, exactly as the
// server returned them (no local transformation of CONTENT).
type HelperToolSchema struct {
Name string
Description string
Properties map[string]any // MCP param name → property object {type,description,default?,...}
Required []string // MCP param names that are required
}
// HelperToolFetcher loads a helper MCP server's tools/list LIVE and returns
// toolName→schema for the given source (e.g. "op-app" for dev app commands,
// "devdoc" for dev doc commands). The schema command injects this so
// dev_schema.go can resolve a command's MCP tool and render its real schema
// without the cli package importing app/transport. Implementations should
// cache per-source per-process so repeated `dws schema dev.*` only hit the
// network once per source.
type HelperToolFetcher func(ctx context.Context, source string) (map[string]HelperToolSchema, error)
// renderHelperSchema builds the `dws schema` payload for helper-only command
// subtrees. Returns (payload, true) when the path targets a helper subtree (so
// the caller skips catalog resolution); (nil, false) otherwise so the caller
// falls back to the discovery catalog.
//
// Leaf commands render the gws-flat object {description, path, source,
// parameters{<kebab>:{type,description,default?,required}}} with all CONTENT
// pulled live from the MCP tool named by the command's `mcp-tool` annotation.
// Group/root paths render a browse listing {path, commands:[...]} from the
// cobra tree (no MCP needed).
func renderHelperSchema(ctx context.Context, root *cobra.Command, rawPath string, fetch HelperToolFetcher) (map[string]any, bool, error) {
if root == nil {
return nil, false, nil
}
tokens := splitSchemaPathTokens(rawPath)
if len(tokens) == 0 || !helperSchemaRoots[tokens[0]] {
return nil, false, nil
}
target, rest, err := root.Find(tokens)
if err != nil || target == nil {
target = root
rest = tokens[1:]
}
// Find resolves to the deepest matching command and returns trailing tokens
// it couldn't match as (sub)commands. Any non-flag leftover means a typo'd
// or unknown subcommand — surface it with the closest group's children.
if unknown := firstNonFlag(rest); unknown != "" {
return map[string]any{
"path": rawPath,
"error": "unknown subcommand \"" + unknown + "\" under \"" + helperCommandPath(target) + "\"",
"available": helperSubcommands(target),
}, true, nil
}
// A runnable leaf → emit its live MCP schema in gws-flat shape.
// A group → browse its subcommands.
if target.Runnable() && !target.HasAvailableSubCommands() {
payload, err := helperLeafSchema(ctx, target, fetch)
return payload, true, err
}
return map[string]any{
"path": helperCommandPath(target),
"commands": helperSubcommands(target),
}, true, nil
}
// helperLeafSchema renders a single leaf command as the gws-flat object,
// fetching its MCP tool schema live. The command must carry an `mcp-tool`
// annotation; commands without one (e.g. `dev connect`, `dev doc search`) are
// not devapp tools and get a clear, non-fatal explanation instead.
func helperLeafSchema(ctx context.Context, cmd *cobra.Command, fetch HelperToolFetcher) (map[string]any, error) {
toolName, source := "", ""
if cmd.Annotations != nil {
toolName = strings.TrimSpace(cmd.Annotations["mcp-tool"])
source = strings.TrimSpace(cmd.Annotations["mcp-source"])
}
// Default source is op-app (dev app commands); dev doc commands annotate
// mcp-source=devdoc to pull from the devdoc MCP server instead.
if source == "" {
source = "op-app"
}
path := helperCommandPath(cmd)
if toolName == "" {
return map[string]any{
"path": path,
"error": "no MCP tool bound to this command; schema is unavailable",
}, nil
}
if fetch == nil {
return map[string]any{
"path": path,
"error": "live MCP schema fetcher not configured",
}, nil
}
tools, err := fetch(ctx, source)
if err != nil {
return nil, fmt.Errorf("fetch %s tool schemas: %w", source, err)
}
tool, ok := tools[toolName]
if !ok {
return map[string]any{
"path": path,
"error": fmt.Sprintf("MCP tool %q not found in %s tools/list", toolName, source),
}, nil
}
return map[string]any{
"description": tool.Description,
"path": path,
"source": "mcp:" + source,
"parameters": helperFlatParameters(tool),
}, nil
}
// helperFlatParameters projects an MCP tool's inputSchema into the gws-flat
// per-parameter object. Keys are kebab-case of the MCP param name (== the CLI
// flag); each value is {type, description, default?, required} with type mapped
// to a JSON-type string, description verbatim from MCP, default only when MCP
// provides one (stringified), and required inline (true iff the param is in the
// tool's required[]).
func helperFlatParameters(tool HelperToolSchema) map[string]any {
required := make(map[string]bool, len(tool.Required))
for _, r := range tool.Required {
required[r] = true
}
params := make(map[string]any, len(tool.Properties))
for name, raw := range tool.Properties {
prop, _ := raw.(map[string]any)
entry := map[string]any{
"type": mcpJSONType(prop),
"description": mcpString(prop, "description"),
"required": required[name],
}
if def, ok := mcpDefault(prop); ok {
entry["default"] = def
}
params[kebabCase(name)] = entry
}
return params
}
// mcpJSONType normalizes the MCP property "type" to a JSON-type string. MCP
// reports standard JSON Schema types; pass them through, defaulting to "string"
// when absent/unknown so the contract is always populated.
func mcpJSONType(prop map[string]any) string {
t, _ := prop["type"].(string)
switch t {
case "string", "integer", "number", "boolean", "array", "object":
return t
default:
return "string"
}
}
// mcpString reads a string field from an MCP property object.
func mcpString(prop map[string]any, key string) string {
if prop == nil {
return ""
}
v, _ := prop[key].(string)
return v
}
// mcpDefault returns the MCP-provided default, stringified, only when present.
// gws renders default as a string; mirror that. Non-string JSON defaults
// (numbers/bools) are formatted with %v so e.g. 0 → "0", true → "true".
func mcpDefault(prop map[string]any) (string, bool) {
if prop == nil {
return "", false
}
v, ok := prop["default"]
if !ok || v == nil {
return "", false
}
switch tv := v.(type) {
case string:
return tv, true
case float64:
// JSON numbers decode to float64; render integers without a fraction.
if tv == float64(int64(tv)) {
return fmt.Sprintf("%d", int64(tv)), true
}
return fmt.Sprintf("%v", tv), true
default:
return fmt.Sprintf("%v", tv), true
}
}
// kebabCase converts an MCP camelCase param name to the CLI flag's kebab form,
// matching how flags are registered in internal/helpers/devapp.go:
//
// eventCallbackUrl → event-callback-url
// unifiedAppId → unified-app-id
// disableSSLVerify → disable-ssl-verify
//
// A boundary is inserted before an uppercase letter that follows a lowercase
// letter or digit, and before the final uppercase of a run that starts a new
// lowercase word (so SSLVerify → ssl-verify, not s-s-l-verify).
func kebabCase(name string) string {
runes := []rune(name)
var b strings.Builder
for i, r := range runes {
if unicode.IsUpper(r) {
prevLowerOrDigit := i > 0 && (unicode.IsLower(runes[i-1]) || unicode.IsDigit(runes[i-1]))
nextLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
if i > 0 && (prevLowerOrDigit || nextLower) {
b.WriteByte('-')
}
b.WriteRune(unicode.ToLower(r))
continue
}
b.WriteRune(r)
}
// Collapse any accidental double dashes and trim, just in case the source
// already contained separators.
out := strings.ReplaceAll(b.String(), "_", "-")
for strings.Contains(out, "--") {
out = strings.ReplaceAll(out, "--", "-")
}
return strings.Trim(out, "-")
}
// helperSubcommands lists a group's runnable children for browse mode, sorted
// by name for deterministic output.
func helperSubcommands(cmd *cobra.Command) []map[string]any {
out := []map[string]any{}
for _, sub := range cmd.Commands() {
if !sub.IsAvailableCommand() || sub.Name() == "help" {
continue
}
out = append(out, map[string]any{
"cli_path": helperCommandPath(sub),
"description": strings.TrimSpace(sub.Short),
})
}
sort.Slice(out, func(i, j int) bool {
return out[i]["cli_path"].(string) < out[j]["cli_path"].(string)
})
return out
}
// helperCommandPath returns the space-joined path from root to cmd, e.g.
// "dev app robot config".
func helperCommandPath(cmd *cobra.Command) string {
parts := []string{}
for c := cmd; c != nil && c.HasParent(); c = c.Parent() {
parts = append([]string{c.Name()}, parts...)
}
return strings.Join(parts, " ")
}
// firstNonFlag returns the first token that is not a flag (does not start with
// "-"), or "" if there is none.
func firstNonFlag(tokens []string) string {
for _, t := range tokens {
if t != "" && !strings.HasPrefix(t, "-") {
return t
}
}
return ""
}
-254
View File
@@ -1,254 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"errors"
"testing"
"github.com/spf13/cobra"
)
// buildHelperTestTree mirrors the shape of the real `dws dev` subtree closely
// enough to exercise the live-schema renderer: a group and leaves carrying the
// `mcp-tool` annotation that names the op-app tool to fetch.
func buildHelperTestTree() *cobra.Command {
root := &cobra.Command{Use: "dws"}
create := &cobra.Command{
Use: "create",
Short: "创建应用",
Annotations: map[string]string{"mcp-tool": "create_dev_app"},
Run: func(*cobra.Command, []string) {},
}
config := &cobra.Command{
Use: "config",
Short: "配置机器人",
Annotations: map[string]string{"mcp-tool": "set_extension_robot_config"},
Run: func(*cobra.Command, []string) {},
}
// A leaf without an mcp-tool annotation (e.g. dev connect / dev doc search).
noTool := &cobra.Command{Use: "connect", Short: "无 MCP 工具", Run: func(*cobra.Command, []string) {}}
robot := &cobra.Command{Use: "robot", Short: "机器人能力"}
robot.AddCommand(config)
app := &cobra.Command{Use: "app", Short: "应用"}
app.AddCommand(create, robot)
dev := &cobra.Command{Use: "dev", Short: "开放平台开发者命令"}
dev.AddCommand(app, noTool)
root.AddCommand(dev)
return root
}
// fakeFetcher returns a canned op-app tools/list so the renderer is exercised
// without network. It mirrors the MCP shape: properties keyed by camelCase param
// name, required[] listing the camelCase names.
func fakeFetcher(tools map[string]HelperToolSchema) HelperToolFetcher {
return func(context.Context, string) (map[string]HelperToolSchema, error) {
return tools, nil
}
}
func robotConfigToolSchema() HelperToolSchema {
return HelperToolSchema{
Name: "set_extension_robot_config",
Description: "创建或更新现有应用的机器人配置",
Properties: map[string]any{
"unifiedAppId": map[string]any{"type": "string", "description": "统一应用 ID"},
"eventCallbackUrl": map[string]any{"type": "string", "description": "事件回调地址"},
"skills": map[string]any{"type": "array", "description": "技能列表"},
"mode": map[string]any{"type": "string", "description": "机器人模式", "enum": []any{"HTTPS", "STREAM", "AISKILL"}},
},
Required: []string{"unifiedAppId"},
}
}
func TestRenderHelperSchema_LeafGwsFlat(t *testing.T) {
root := buildHelperTestTree()
fetch := fakeFetcher(map[string]HelperToolSchema{
"set_extension_robot_config": robotConfigToolSchema(),
})
payload, ok, err := renderHelperSchema(context.Background(), root, "dev app robot config", fetch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected helper renderer to claim the path")
}
// Flat top-level: description / path / source / parameters; no wrapper.
if payload["description"] != "创建或更新现有应用的机器人配置" {
t.Fatalf("description = %v", payload["description"])
}
if payload["path"] != "dev app robot config" {
t.Fatalf("path = %v", payload["path"])
}
if payload["source"] != "mcp:op-app" {
t.Fatalf("source = %v", payload["source"])
}
for _, leaked := range []string{"kind", "tool", "product", "helper"} {
if _, present := payload[leaked]; present {
t.Fatalf("gws-flat output must not carry %q wrapper key", leaked)
}
}
params, _ := payload["parameters"].(map[string]any)
if params == nil {
t.Fatalf("no parameters: %#v", payload)
}
// Keys are kebab-case of the MCP param name == the CLI flag.
uid, _ := params["unified-app-id"].(map[string]any)
if uid == nil {
t.Fatalf("missing unified-app-id param: %#v", params)
}
if uid["type"] != "string" || uid["required"] != true {
t.Fatalf("unified-app-id = %#v, want string+required", uid)
}
if _, hasDefault := uid["default"]; hasDefault {
t.Fatal("unified-app-id must not carry a default (MCP provides none)")
}
cb, _ := params["event-callback-url"].(map[string]any)
if cb == nil || cb["required"] != false {
t.Fatalf("event-callback-url = %#v, want required=false", cb)
}
skills, _ := params["skills"].(map[string]any)
if skills == nil || skills["type"] != "array" {
t.Fatalf("skills = %#v, want array", skills)
}
mode, _ := params["mode"].(map[string]any)
if mode == nil || mode["type"] != "string" {
t.Fatalf("mode = %#v, want string", mode)
}
if _, hasDefault := mode["default"]; hasDefault {
t.Fatalf("mode default = %v, want none", mode["default"])
}
if mode["required"] != false {
t.Fatalf("mode required = %v, want false", mode["required"])
}
}
func TestRenderHelperSchema_Group(t *testing.T) {
root := buildHelperTestTree()
payload, ok, err := renderHelperSchema(context.Background(), root, "dev app", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
if payload["path"] != "dev app" {
t.Fatalf("path = %v", payload["path"])
}
cmds, _ := payload["commands"].([]map[string]any)
if len(cmds) != 2 { // create + robot
t.Fatalf("commands count = %d, want 2", len(cmds))
}
}
func TestRenderHelperSchema_NoAnnotation(t *testing.T) {
root := buildHelperTestTree()
payload, ok, err := renderHelperSchema(context.Background(), root, "dev connect", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
if payload["error"] == nil {
t.Fatalf("expected a clear no-MCP-tool error, got %#v", payload)
}
if _, present := payload["parameters"]; present {
t.Fatal("no-tool command must not emit parameters")
}
}
func TestRenderHelperSchema_UnknownSubcommand(t *testing.T) {
root := buildHelperTestTree()
payload, ok, err := renderHelperSchema(context.Background(), root, "dev app nope", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
if payload["error"] == nil {
t.Fatalf("expected error for unknown subcommand, got %#v", payload)
}
if avail, _ := payload["available"].([]map[string]any); len(avail) == 0 {
t.Fatal("expected available subcommands listed")
}
}
func TestRenderHelperSchema_ToolMissingInList(t *testing.T) {
root := buildHelperTestTree()
// Fetcher returns an empty list — the annotated tool isn't present.
payload, ok, err := renderHelperSchema(context.Background(), root, "dev app create", fakeFetcher(map[string]HelperToolSchema{}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected claim")
}
if payload["error"] == nil {
t.Fatalf("expected not-found error, got %#v", payload)
}
}
func TestRenderHelperSchema_FetchError(t *testing.T) {
root := buildHelperTestTree()
failing := func(context.Context, string) (map[string]HelperToolSchema, error) {
return nil, errors.New("network down")
}
_, ok, err := renderHelperSchema(context.Background(), root, "dev app create", failing)
if !ok {
t.Fatal("expected claim even on fetch error")
}
if err == nil {
t.Fatal("expected the fetch error to surface")
}
}
func TestRenderHelperSchema_NonHelperPathDeclined(t *testing.T) {
root := buildHelperTestTree()
if _, ok, _ := renderHelperSchema(context.Background(), root, "ding.message.send", nil); ok {
t.Fatal("non-helper path must not be claimed by the helper renderer")
}
}
func TestKebabCase(t *testing.T) {
cases := map[string]string{
"eventCallbackUrl": "event-callback-url",
"unifiedAppId": "unified-app-id",
"disableSSLVerify": "disable-ssl-verify",
"mode": "mode",
"skills": "skills",
"i18nName": "i18n-name",
}
for in, want := range cases {
if got := kebabCase(in); got != want {
t.Errorf("kebabCase(%q) = %q, want %q", in, got, want)
}
}
}
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli
import (
"testing"
"time"
"github.com/spf13/pflag"
)
func TestRuntimeFlagContractCoversPflagFamilies(t *testing.T) {
flags := pflag.NewFlagSet("schema-types", pflag.ContinueOnError)
flags.Uint("uint", 0, "")
flags.Count("count", "")
flags.Float64("ratio", 0, "")
flags.IntSlice("ids", nil, "")
flags.DurationSlice("windows", nil, "")
flags.Duration("timeout", 0, "")
flags.StringToString("labels", nil, "")
tests := []struct {
name string
wantType string
wantDefault string
}{
{name: "uint", wantType: "integer"},
{name: "count", wantType: "integer"},
{name: "ratio", wantType: "number"},
{name: "ids", wantType: "array"},
{name: "windows", wantType: "array"},
{name: "timeout", wantType: "string"},
{name: "labels", wantType: "string"},
}
for _, test := range tests {
flag := flags.Lookup(test.name)
if got := runtimeFlagCLIType(flag); got != test.wantType {
t.Errorf("--%s type = %q, want %q", test.name, got, test.wantType)
}
if got := runtimeFlagDefault(flag); got != test.wantDefault {
t.Errorf("--%s default = %q, want %q", test.name, got, test.wantDefault)
}
}
flags.Duration("nonzero-timeout", 5*time.Second, "")
if got := runtimeFlagDefault(flags.Lookup("nonzero-timeout")); got != "5s" {
t.Fatalf("non-zero duration default = %q, want 5s", got)
}
}
+368
View File
@@ -0,0 +1,368 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"embed"
"encoding/json"
"strings"
"sync"
"sync/atomic"
)
//go:generate go run ../generator/cmd_schema_agent_metadata -root ../.. -registry internal/cli/schema_command_registry.json -output-dir schema_agent_metadata -audit-output schema_agent_metadata_audit.json
// Rebuild all dependencies so the Catalog compiler cannot reuse the cli
// package cached by the preceding metadata generator with the old embedded
// JSON files.
//go:generate go run -a ../generator/cmd_schema_catalog -root ../.. -output schema_catalog.json
//go:embed schema_agent_metadata/*.json
var embeddedAgentMetadataFS embed.FS
const embeddedAgentMetadataSource = "embedded-skill-metadata"
type embeddedAgentMetadata struct {
Version int `json:"version"`
SourceHash string `json:"source_hash"`
SurfaceHash string `json:"surface_hash,omitempty"`
Coverage embeddedAgentMetadataCoverage `json:"coverage"`
Products map[string]agentProductMetadata `json:"products"`
Domains []string `json:"domains"`
Tools map[string]agentToolMetadata `json:"tools"`
}
type embeddedAgentMetadataCoverage struct {
SurfaceProducts int `json:"surface_products,omitempty"`
ProductsWithMetadata int `json:"products_with_metadata"`
SurfaceTools int `json:"surface_tools,omitempty"`
ToolsWithMetadata int `json:"tools_with_metadata"`
ToolsWithSummary int `json:"tools_with_agent_summary,omitempty"`
ToolsWithUseWhen int `json:"tools_with_use_when,omitempty"`
ToolsWithAvoidWhen int `json:"tools_with_avoid_when,omitempty"`
ToolsWithExamples int `json:"tools_with_examples,omitempty"`
ToolsWithInterfaceMode int `json:"tools_with_interface_mode,omitempty"`
UnmatchedSkillTools int `json:"unmatched_skill_tools,omitempty"`
UnreviewedSkillTools int `json:"unreviewed_skill_tools,omitempty"`
}
type agentProductMetadata struct {
AgentSummary string `json:"agent_summary,omitempty"`
AgentSummarySource string `json:"agent_summary_source,omitempty"`
UseWhen []string `json:"use_when,omitempty"`
AvoidWhen []string `json:"avoid_when,omitempty"`
SourceRefs []string `json:"source_refs,omitempty"`
FieldProvenance map[string]FieldProvenance `json:"field_provenance,omitempty"`
}
type agentToolMetadata struct {
AgentSummary string `json:"agent_summary,omitempty"`
AgentSummarySource string `json:"agent_summary_source,omitempty"`
UseWhen []string `json:"use_when,omitempty"`
AvoidWhen []string `json:"avoid_when,omitempty"`
Prerequisites []string `json:"prerequisites,omitempty"`
Tips []string `json:"tips,omitempty"`
Effect string `json:"effect,omitempty"`
EffectSource string `json:"effect_source,omitempty"`
Risk string `json:"risk,omitempty"`
Confirmation string `json:"confirmation,omitempty"`
Idempotency string `json:"idempotency,omitempty"`
WorkflowRefs []string `json:"workflow_refs,omitempty"`
Examples []string `json:"examples,omitempty"`
Reviewed *bool `json:"reviewed,omitempty"`
SourceRefs []string `json:"source_refs,omitempty"`
InterfaceRef *embeddedMCPInterfaceRef `json:"interface_ref,omitempty"`
InterfaceMode string `json:"interface_mode,omitempty"`
Availability string `json:"availability,omitempty"`
InterfaceReason string `json:"interface_reason,omitempty"`
FieldProvenance map[string]FieldProvenance `json:"field_provenance,omitempty"`
}
type embeddedAgentMetadataDomain struct {
ProductID string `json:"product_id"`
Tools map[string]agentToolMetadata `json:"tools"`
}
var runtimeEmbeddedAgentMetadataLazy struct {
once sync.Once
metadata embeddedAgentMetadata
}
var runtimeEmbeddedAgentMetadataLazyLoadCount atomic.Uint64
// runtimeAgentMetadata parses the generated Agent metadata on first Schema
// assembly only. Keeping the sync.Once at the access boundary ensures package
// initialization and ordinary CLI commands never deserialize the embedded
// fragments.
func runtimeAgentMetadata() embeddedAgentMetadata {
runtimeEmbeddedAgentMetadataLazy.once.Do(func() {
runtimeEmbeddedAgentMetadataLazyLoadCount.Add(1)
runtimeEmbeddedAgentMetadataLazy.metadata = loadEmbeddedAgentMetadata()
})
return runtimeEmbeddedAgentMetadataLazy.metadata
}
func loadEmbeddedAgentMetadata() embeddedAgentMetadata {
var metadata embeddedAgentMetadata
index, err := embeddedAgentMetadataFS.ReadFile("schema_agent_metadata/index.json")
if err != nil || json.Unmarshal(index, &metadata) != nil {
return emptyEmbeddedAgentMetadata()
}
metadata.Tools = map[string]agentToolMetadata{}
for _, domain := range metadata.Domains {
domain = strings.TrimSpace(domain)
if domain == "" || strings.Contains(domain, "/") || strings.Contains(domain, "\\") {
return emptyEmbeddedAgentMetadata()
}
data, err := embeddedAgentMetadataFS.ReadFile("schema_agent_metadata/" + domain + ".json")
if err != nil {
return emptyEmbeddedAgentMetadata()
}
var fragment embeddedAgentMetadataDomain
if err := json.Unmarshal(data, &fragment); err != nil || strings.TrimSpace(fragment.ProductID) != domain {
return emptyEmbeddedAgentMetadata()
}
for path, tool := range fragment.Tools {
metadata.Tools[path] = tool
}
}
if metadata.Products == nil {
metadata.Products = map[string]agentProductMetadata{}
}
return metadata
}
func emptyEmbeddedAgentMetadata() embeddedAgentMetadata {
return embeddedAgentMetadata{
Products: map[string]agentProductMetadata{},
Tools: map[string]agentToolMetadata{},
}
}
// agentToolContractForPathsFromMetadata is the sole typed adapter from generated Agent
// metadata to runtime contract assembly. Path resolution happens once; all
// consumers receive the same resolved safety, interface, selection and
// provenance values without performing downstream map merges.
func agentToolContractForPathsFromMetadata(source embeddedAgentMetadata, paths ...string) (SafetySpec, InterfaceSpec, SelectionSpec, map[string]FieldProvenance, bool) {
metadata, ok := lookupAgentToolMetadataFrom(source, paths...)
if !ok {
return SafetySpec{}, InterfaceSpec{}, SelectionSpec{}, nil, false
}
safety := SafetySpec{
Effect: strings.TrimSpace(metadata.Effect),
EffectSource: strings.TrimSpace(metadata.EffectSource),
Risk: strings.TrimSpace(metadata.Risk),
Confirmation: strings.TrimSpace(metadata.Confirmation),
Idempotency: strings.TrimSpace(metadata.Idempotency),
}
interfaceSpec := InterfaceSpec{
Mode: strings.TrimSpace(metadata.InterfaceMode),
Availability: strings.TrimSpace(metadata.Availability),
Reason: strings.TrimSpace(metadata.InterfaceReason),
}
if metadata.InterfaceRef != nil {
interfaceSpec.Ref = &InterfaceRefSpec{
ProductID: strings.TrimSpace(metadata.InterfaceRef.ProductID),
RPCName: strings.TrimSpace(metadata.InterfaceRef.RPCName),
}
}
selection := agentToolSelection(metadata)
provenance := resolvedAgentToolProvenance(metadata.FieldProvenance, interfaceSpec, selection)
return safety, interfaceSpec, selection, provenance, true
}
// resolvedAgentToolProvenance is the typed source adapter for generated Agent
// metadata. The generator stores concrete interface refs in its compact
// "product.rpc" identity form and stores reviewed no-ref dispositions as JSON
// null. The final typed contract stores an object or JSON null, so project the
// concrete compact identity here. Missing provenance is never synthesized:
// constructors and snapshot loaders remain validate-only and fail closed.
func resolvedAgentToolProvenance(source map[string]FieldProvenance, interfaceSpec InterfaceSpec, selection SelectionSpec) map[string]FieldProvenance {
out := cloneFieldProvenance(source)
if provenance, ok := out["interface_ref"]; ok {
out["interface_ref"] = projectAgentInterfaceRefProvenance(provenance, interfaceSpec.Ref)
}
return out
}
func projectAgentInterfaceRefProvenance(provenance FieldProvenance, ref *InterfaceRefSpec) FieldProvenance {
finalValue, _ := json.Marshal(ref)
legacy := "<none>"
if ref != nil {
legacy = strings.TrimSpace(ref.ProductID) + "." + strings.TrimSpace(ref.RPCName)
}
legacyValue, _ := json.Marshal(legacy)
project := func(value json.RawMessage) json.RawMessage {
if string(value) == string(legacyValue) || string(value) == string(finalValue) {
return append(json.RawMessage(nil), finalValue...)
}
// Keep a disagreeing source value untouched. ToolSpec validation will
// then fail instead of this adapter laundering a resolver conflict.
return value
}
provenance.Value = project(provenance.Value)
for index := range provenance.Candidates {
candidate := &provenance.Candidates[index]
if candidate.Selected != nil && *candidate.Selected {
candidate.Value = project(candidate.Value)
}
}
return provenance
}
func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) FieldProvenance {
raw, err := json.Marshal(value)
if err != nil {
raw = json.RawMessage("null")
}
selected := true
return FieldProvenance{
Value: append(json.RawMessage(nil), raw...),
Source: strings.TrimSpace(source),
SourceRef: strings.TrimSpace(sourceRef),
Precedence: strings.TrimSpace(precedence),
Resolution: strings.TrimSpace(resolution),
ReviewReason: strings.TrimSpace(reviewReason),
Candidates: []FieldCandidateProvenance{{
Value: append(json.RawMessage(nil), raw...),
Source: strings.TrimSpace(source),
SourceRef: strings.TrimSpace(sourceRef),
Precedence: strings.TrimSpace(precedence),
ReviewReason: strings.TrimSpace(reviewReason),
Selected: &selected,
}},
}
}
// agentProductSelectionForIDsFromMetadata exposes generated product routing
// through the same typed SelectionSpec used by ToolSpec.
func agentProductSelectionForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (SelectionSpec, bool) {
selection, _, ok := agentProductContractForIDsFromMetadata(source, ids...)
return selection, ok
}
func agentProductContractForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (SelectionSpec, map[string]FieldProvenance, bool) {
for _, id := range ids {
metadata, ok := source.Products[strings.TrimSpace(id)]
if !ok {
continue
}
selection := SelectionSpec{
AgentSummary: strings.TrimSpace(metadata.AgentSummary),
AgentSummarySource: strings.TrimSpace(metadata.AgentSummarySource),
UseWhen: cloneOptionalStrings(metadata.UseWhen),
AvoidWhen: cloneOptionalStrings(metadata.AvoidWhen),
SourceRefs: cloneOptionalStrings(metadata.SourceRefs),
MetadataSource: embeddedAgentMetadataSource,
}.normalized()
return selection, cloneFieldProvenance(metadata.FieldProvenance), true
}
return SelectionSpec{}, nil, false
}
func agentToolSelection(metadata agentToolMetadata) SelectionSpec {
var reviewed *bool
if metadata.Reviewed != nil {
value := *metadata.Reviewed
reviewed = &value
}
return SelectionSpec{
AgentSummary: strings.TrimSpace(metadata.AgentSummary),
AgentSummarySource: strings.TrimSpace(metadata.AgentSummarySource),
UseWhen: cloneOptionalStrings(metadata.UseWhen),
AvoidWhen: cloneOptionalStrings(metadata.AvoidWhen),
Prerequisites: cloneOptionalStrings(metadata.Prerequisites),
Tips: cloneOptionalStrings(metadata.Tips),
WorkflowRefs: cloneOptionalStrings(metadata.WorkflowRefs),
Examples: cloneOptionalStrings(metadata.Examples),
Reviewed: reviewed,
SourceRefs: cloneOptionalStrings(metadata.SourceRefs),
MetadataSource: embeddedAgentMetadataSource,
}.normalized()
}
func cloneFieldProvenance(source map[string]FieldProvenance) map[string]FieldProvenance {
if len(source) == 0 {
return nil
}
out := make(map[string]FieldProvenance, len(source))
for field, provenance := range source {
provenance.Value = append(json.RawMessage(nil), provenance.Value...)
provenance.Candidates = cloneFieldCandidates(provenance.Candidates)
provenance.OverriddenCandidates = cloneFieldCandidates(provenance.OverriddenCandidates)
out[field] = provenance
}
return out
}
func cloneFieldCandidates(source []FieldCandidateProvenance) []FieldCandidateProvenance {
if len(source) == 0 {
return nil
}
out := make([]FieldCandidateProvenance, len(source))
for index, candidate := range source {
candidate.Value = append(json.RawMessage(nil), candidate.Value...)
if candidate.Selected != nil {
value := *candidate.Selected
candidate.Selected = &value
}
out[index] = candidate
}
return out
}
func lookupAgentToolMetadataFrom(source embeddedAgentMetadata, paths ...string) (agentToolMetadata, bool) {
seen := map[string]bool{}
for _, path := range paths {
for _, candidate := range []string{
strings.TrimSpace(path),
strings.Join(splitSchemaPathTokens(path), " "),
} {
if candidate == "" || seen[candidate] {
continue
}
seen[candidate] = true
if metadata, ok := source.Tools[candidate]; ok {
return metadata, true
}
}
}
return agentToolMetadata{}, false
}
func agentMetadataSummaryFrom(metadata embeddedAgentMetadata) map[string]any {
summary := map[string]any{
"source": embeddedAgentMetadataSource,
"version": metadata.Version,
"source_hash": strings.TrimSpace(metadata.SourceHash),
"products_with_metadata": len(metadata.Products),
"tools_with_metadata": len(metadata.Tools),
}
if metadata.SurfaceHash != "" {
summary["surface_hash"] = metadata.SurfaceHash
}
coverage := metadata.Coverage
if coverage.SurfaceProducts > 0 {
summary["surface_products"] = coverage.SurfaceProducts
}
if coverage.SurfaceTools > 0 {
summary["surface_tools"] = coverage.SurfaceTools
}
if coverage.ToolsWithSummary > 0 {
summary["tools_with_agent_summary"] = coverage.ToolsWithSummary
}
if coverage.UnmatchedSkillTools > 0 {
summary["unmatched_skill_tools"] = coverage.UnmatchedSkillTools
}
return summary
}
@@ -0,0 +1,797 @@
{
"product_id": "aisearch",
"tools": {
"aisearch behavior": {
"agent_summary": "搜索发送/创建/分享/编辑/接收等明确行为记录",
"agent_summary_source": "dws-agent-selection/aisearch",
"availability": "available",
"avoid_when": [
"只按主题找内容本身时用 aisearch enterprise",
"没有行为动作词时不要选用本工具"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws aisearch behavior --types mail --behavior-type send --direction \"我-\u003e汐峰\" --format json",
"dws aisearch behavior --queries \"智能化方案\" --types document --behavior-type create --format json"
],
"field_provenance": {
"agent_summary": {
"value": "搜索发送/创建/分享/编辑/接收等明确行为记录",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "搜索发送/创建/分享/编辑/接收等明确行为记录",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise_behavior.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"只按主题找内容本身时用 aisearch enterprise",
"没有行为动作词时不要选用本工具"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"只按主题找内容本身时用 aisearch enterprise",
"没有行为动作词时不要选用本工具"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws aisearch behavior --types mail --behavior-type send --direction \"我-\u003e汐峰\" --format json",
"dws aisearch behavior --queries \"智能化方案\" --types document --behavior-type create --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws aisearch behavior --types mail --behavior-type send --direction \"我-\u003e汐峰\" --format json",
"dws aisearch behavior --queries \"智能化方案\" --types document --behavior-type create --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise_behavior.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "aisearch.search_enterprise_behavior",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise_behavior.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "aisearch.search_enterprise_behavior",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise_behavior.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"用户明确问我/某人发过、发给、收到、创建、分享、编辑过什么"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"用户明确问我/某人发过、发给、收到、创建、分享、编辑过什么"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "aisearch",
"rpc_name": "search_enterprise_behavior"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"Cobra help: dws aisearch behavior --help",
"internal/cli/schema_command_registry.json",
"internal/cli/schema_command_registry.json#aisearch.search_enterprise_behavior",
"internal/cli/schema_hints/metadata/aisearch.json",
"internal/cli/schema_hints/selection/aisearch.json",
"internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise_behavior",
"live-dws-schema:aisearch.search_enterprise_behavior",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/aisearch.md"
],
"use_when": [
"用户明确问我/某人发过、发给、收到、创建、分享、编辑过什么"
]
},
"aisearch enterprise": {
"agent_summary": "搜索企业内部知识与相关内容(文档/IM/日历/待办/纪要/日志/邮件等)",
"agent_summary_source": "dws-agent-selection/aisearch",
"availability": "available",
"avoid_when": [
"问“我发给谁/谁发给我/我创建过”等行为追溯时用 aisearch behavior",
"企业找人时用 aisearch person",
"已知具体资源 ID 要读写时改用对应产品命令"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws aisearch enterprise --queries \"智能化方案\" --types document --format json",
"dws aisearch enterprise --queries \"OKR\" --types mail --time-range \"最近\" --format json"
],
"field_provenance": {
"agent_summary": {
"value": "搜索企业内部知识与相关内容(文档/IM/日历/待办/纪要/日志/邮件等)",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "搜索企业内部知识与相关内容(文档/IM/日历/待办/纪要/日志/邮件等)",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input."
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"问“我发给谁/谁发给我/我创建过”等行为追溯时用 aisearch behavior",
"企业找人时用 aisearch person",
"已知具体资源 ID 要读写时改用对应产品命令"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"问“我发给谁/谁发给我/我创建过”等行为追溯时用 aisearch behavior",
"企业找人时用 aisearch person",
"已知具体资源 ID 要读写时改用对应产品命令"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input."
},
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws aisearch enterprise --queries \"智能化方案\" --types document --format json",
"dws aisearch enterprise --queries \"OKR\" --types mail --time-range \"最近\" --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws aisearch enterprise --queries \"智能化方案\" --types document --format json",
"dws aisearch enterprise --queries \"OKR\" --types mail --time-range \"最近\" --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input."
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "aisearch.search_enterprise",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "aisearch.search_enterprise",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"按主题找资料、方案、文档、消息、邮件等内容,且关注“有什么内容”",
"用户显式给出时间词或类型词时分别映射到 --time-range / --types"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"按主题找资料、方案、文档、消息、邮件等内容,且关注“有什么内容”",
"用户显式给出时间词或类型词时分别映射到 --time-range / --types"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "aisearch",
"rpc_name": "search_enterprise"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"Cobra help: dws aisearch enterprise --help",
"internal/cli/schema_command_registry.json",
"internal/cli/schema_command_registry.json#aisearch.search_enterprise",
"internal/cli/schema_hints/metadata/aisearch.json",
"internal/cli/schema_hints/selection/aisearch.json",
"internal/cli/schema_mcp_metadata.json#tools.aisearch.search_enterprise",
"live-dws-schema:aisearch.search_enterprise",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/aisearch.md"
],
"use_when": [
"按主题找资料、方案、文档、消息、邮件等内容,且关注“有什么内容”",
"用户显式给出时间词或类型词时分别映射到 --time-range / --types"
]
},
"aisearch person": {
"agent_summary": "企业内找人:按姓名/部门/职位/职责/上下级/手机号/工号筛选",
"agent_summary_source": "dws-agent-selection/aisearch",
"availability": "available",
"avoid_when": [
"已有 userId 只需详情时用 contact user get",
"精确通讯录关键词搜同事/好友且不涉及职责语义时可用 contact user search",
"搜企业知识内容时用 aisearch enterprise;搜行为记录时用 aisearch behavior"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws aisearch person --keyword \"张三\" --dimension name --format json",
"dws aisearch person --keyword \"五道\" --dimension supervisor --format json"
],
"field_provenance": {
"agent_summary": {
"value": "企业内找人:按姓名/部门/职位/职责/上下级/手机号/工号筛选",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "企业内找人:按姓名/部门/职位/职责/上下级/手机号/工号筛选",
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.enterprise_person_search.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"已有 userId 只需详情时用 contact user get",
"精确通讯录关键词搜同事/好友且不涉及职责语义时可用 contact user search",
"搜企业知识内容时用 aisearch enterprise;搜行为记录时用 aisearch behavior"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"已有 userId 只需详情时用 contact user get",
"精确通讯录关键词搜同事/好友且不涉及职责语义时可用 contact user search",
"搜企业知识内容时用 aisearch enterprise;搜行为记录时用 aisearch behavior"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws aisearch person --keyword \"张三\" --dimension name --format json",
"dws aisearch person --keyword \"五道\" --dimension supervisor --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws aisearch person --keyword \"张三\" --dimension name --format json",
"dws aisearch person --keyword \"五道\" --dimension supervisor --format json"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.enterprise_person_search.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "aisearch.enterprise_person_search",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.enterprise_person_search.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "aisearch.enterprise_person_search",
"source": "internal/cli/schema_mcp_metadata.json#tools.aisearch.enterprise_person_search.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/aisearch marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"找人、谁负责某事、查上级/下级、按手机号或工号定位人员",
"需要把维度词映射到 --dimension,关键词只保留目标实体"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"找人、谁负责某事、查上级/下级、按手机号或工号定位人员",
"需要把维度词映射到 --dimension,关键词只保留目标实体"
],
"source": "internal/cli/schema_hints/selection/aisearch.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "aisearch",
"rpc_name": "enterprise_person_search"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"Cobra help: dws aisearch person --help",
"internal/cli/schema_command_registry.json",
"internal/cli/schema_command_registry.json#aisearch.enterprise_person_search",
"internal/cli/schema_hints/metadata/aisearch.json",
"internal/cli/schema_hints/selection/aisearch.json",
"internal/cli/schema_mcp_metadata.json#tools.aisearch.enterprise_person_search",
"live-dws-schema:aisearch.enterprise_person_search",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/aisearch.md",
"skills/mono/references/products/mail.md"
],
"use_when": [
"找人、谁负责某事、查上级/下级、按手机号或工号定位人员",
"需要把维度词映射到 --dimension,关键词只保留目标实体"
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,794 @@
{
"product_id": "audit",
"tools": {
"audit export": {
"agent_summary": "按日期范围导出本地操作审计日志(jsonl 或 csv)",
"agent_summary_source": "dws-agent-selection/audit",
"availability": "available",
"avoid_when": [
"只看最近几条用 audit tail",
"只校验哈希链完整性用 audit verify"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws audit export --format jsonl",
"dws audit export --since 2026-07-01 --until 2026-07-14 --format csv"
],
"field_provenance": {
"agent_summary": {
"value": "按日期范围导出本地操作审计日志(jsonl 或 csv)",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。",
"candidates": [
{
"value": "按日期范围导出本地操作审计日志(jsonl 或 csv)",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"avoid_when": {
"value": [
"只看最近几条用 audit tail",
"只校验哈希链完整性用 audit verify"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。",
"candidates": [
{
"value": [
"只看最近几条用 audit tail",
"只校验哈希链完整性用 audit verify"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"examples": {
"value": [
"dws audit export --format jsonl",
"dws audit export --since 2026-07-01 --until 2026-07-14 --format csv"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。",
"candidates": [
{
"value": [
"dws audit export --format jsonl",
"dws audit export --since 2026-07-01 --until 2026-07-14 --format csv"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"interface_mode": {
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"interface_reason": {
"value": "命令读取并导出本地审计日志文件,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "命令读取并导出本地审计日志文件,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode local forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode local forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log export from #555; no remote RPC.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log export from #555; no remote RPC."
}
]
},
"use_when": {
"value": [
"需要把本地审计日志导出为 jsonl/csv,或按 --since/--until 取一段时间"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。",
"candidates": [
{
"value": [
"需要把本地审计日志导出为 jsonl/csv,或按 --since/--until 取一段时间"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读导出,示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "local",
"interface_reason": "命令读取并导出本地审计日志文件,不绑定 pinned MCP RPC",
"reviewed": true,
"risk": "low",
"source_refs": [
"cobra-help:dws audit export --help",
"internal/app/audit_command.go",
"internal/cli/schema_command_registry.json#audit.export",
"internal/cli/schema_hints/metadata/audit.json",
"internal/cli/schema_hints/selection/audit.json"
],
"use_when": [
"需要把本地审计日志导出为 jsonl/csv,或按 --since/--until 取一段时间"
]
},
"audit tail": {
"agent_summary": "查看本地操作审计日志最近 N 条记录",
"agent_summary_source": "dws-agent-selection/audit",
"availability": "available",
"avoid_when": [
"需要按日期范围整段导出用 audit export",
"需要校验哈希链用 audit verify"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws audit tail",
"dws audit tail --lines 50"
],
"field_provenance": {
"agent_summary": {
"value": "查看本地操作审计日志最近 N 条记录",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。",
"candidates": [
{
"value": "查看本地操作审计日志最近 N 条记录",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"avoid_when": {
"value": [
"需要按日期范围整段导出用 audit export",
"需要校验哈希链用 audit verify"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。",
"candidates": [
{
"value": [
"需要按日期范围整段导出用 audit export",
"需要校验哈希链用 audit verify"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"examples": {
"value": [
"dws audit tail",
"dws audit tail --lines 50"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。",
"candidates": [
{
"value": [
"dws audit tail",
"dws audit tail --lines 50"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"interface_mode": {
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"interface_reason": {
"value": "命令读取本地审计日志尾部,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "命令读取本地审计日志尾部,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode local forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode local forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit log tail from #555; no remote RPC.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit log tail from #555; no remote RPC."
}
]
},
"use_when": {
"value": [
"需要快速查看最近写入的审计记录(默认最近 20 条)"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。",
"candidates": [
{
"value": [
"需要快速查看最近写入的审计记录(默认最近 20 条)"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读查看,示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "local",
"interface_reason": "命令读取本地审计日志尾部,不绑定 pinned MCP RPC",
"reviewed": true,
"risk": "low",
"source_refs": [
"cobra-help:dws audit tail --help",
"internal/app/audit_command.go",
"internal/cli/schema_command_registry.json#audit.tail",
"internal/cli/schema_hints/metadata/audit.json",
"internal/cli/schema_hints/selection/audit.json"
],
"use_when": [
"需要快速查看最近写入的审计记录(默认最近 20 条)"
]
},
"audit verify": {
"agent_summary": "校验本地审计日志文件的哈希链完整性",
"agent_summary_source": "dws-agent-selection/audit",
"availability": "available",
"avoid_when": [
"只浏览或导出日志内容时用 audit tail / audit export"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws audit verify",
"dws audit verify --file /path/to/audit.jsonl"
],
"field_provenance": {
"agent_summary": {
"value": "校验本地审计日志文件的哈希链完整性",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。",
"candidates": [
{
"value": "校验本地审计日志文件的哈希链完整性",
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"avoid_when": {
"value": [
"只浏览或导出日志内容时用 audit tail / audit export"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。",
"candidates": [
{
"value": [
"只浏览或导出日志内容时用 audit tail / audit export"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"examples": {
"value": [
"dws audit verify",
"dws audit verify --file /path/to/audit.jsonl"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。",
"candidates": [
{
"value": [
"dws audit verify",
"dws audit verify --file /path/to/audit.jsonl"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"interface_mode": {
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "local",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"interface_reason": {
"value": "命令校验本地审计日志哈希链,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "命令校验本地审计日志哈希链,不绑定 pinned MCP RPC",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode local forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode local forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Local audit hash-chain verify from #555; no remote RPC.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Local audit hash-chain verify from #555; no remote RPC."
}
]
},
"use_when": {
"value": [
"怀疑审计文件被篡改,或需要确认最新/指定文件哈希链是否完整"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。",
"candidates": [
{
"value": [
"怀疑审计文件被篡改,或需要确认最新/指定文件哈希链是否完整"
],
"source": "internal/cli/schema_hints/selection/audit.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "纳入 #555 公开 audit leaf 的 Agent 选型文案;本地只读校验,示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "local",
"interface_reason": "命令校验本地审计日志哈希链,不绑定 pinned MCP RPC",
"reviewed": true,
"risk": "low",
"source_refs": [
"cobra-help:dws audit verify --help",
"internal/app/audit_command.go",
"internal/cli/schema_command_registry.json#audit.verify",
"internal/cli/schema_hints/metadata/audit.json",
"internal/cli/schema_hints/selection/audit.json"
],
"use_when": [
"怀疑审计文件被篡改,或需要确认最新/指定文件哈希链是否完整"
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,281 @@
{
"product_id": "devdoc",
"tools": {
"devdoc article search": {
"agent_summary": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)",
"agent_summary_source": "dws-agent-selection/devdoc",
"availability": "available",
"avoid_when": [
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
"要执行开放平台应用配置变更时用 dev"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws devdoc article search --query \"OAuth2 接入\" --format json",
"dws devdoc article search --query \"errcode 40078\" --format json"
],
"field_provenance": {
"agent_summary": {
"value": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)",
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)",
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword."
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
"要执行开放平台应用配置变更时用 dev"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
"要执行开放平台应用配置变更时用 dev"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword."
},
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws devdoc article search --query \"OAuth2 接入\" --format json",
"dws devdoc article search --query \"errcode 40078\" --format json"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws devdoc article search --query \"OAuth2 接入\" --format json",
"dws devdoc article search --query \"errcode 40078\" --format json"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword."
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "devdoc.search_open_platform_docs",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"resolution": "highest_precedence",
"candidates": [
{
"value": "devdoc.search_open_platform_docs",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"selected": true
},
{
"value": "devdoc.search_open_platform_docs",
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
],
"source": "internal/cli/schema_hints/selection/devdoc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "devdoc",
"rpc_name": "search_open_platform_docs"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"CommandRegistry:canonical_path=devdoc.search_open_platform_docs_rag",
"Skill:skills/mono/references/products/devdoc.md",
"cobra-help:dws devdoc article search",
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.devdoc.prod.json#devdoc.search_open_platform_docs",
"internal/cli/schema_command_registry.json#devdoc.search_open_platform_docs_rag",
"internal/cli/schema_hints/imported/wukong.json",
"internal/cli/schema_hints/metadata/devdoc.json",
"internal/cli/schema_hints/selection/devdoc.json",
"internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag",
"live-dws-schema:devdoc.search_open_platform_docs_rag",
"skills/mono/references/products/devdoc.md",
"skills/mono/references/products/simple.md",
"structured-hint:internal/cli/schema_hints/imported/wukong.json#devdoc.search_open_platform_docs_rag",
"structured-hint:internal/cli/schema_hints/selection-review.json#devdoc.search_open_platform_docs_rag"
],
"use_when": [
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
]
}
}
}
@@ -0,0 +1,584 @@
{
"product_id": "ding",
"tools": {
"ding message recall": {
"agent_summary": "撤回已发送的机器人 DING",
"agent_summary_source": "dws-agent-selection/ding",
"availability": "available",
"avoid_when": [
"需要以用户身份撤回 DING 时不要使用本命令"
],
"confirmation": "not_required",
"effect": "write",
"effect_source": "agent-hint",
"examples": [
"dws ding message recall --robot-code \u003cROBOT_CODE\u003e --id \u003cOPEN_DING_ID\u003e --format json"
],
"field_provenance": {
"agent_summary": {
"value": "撤回已发送的机器人 DING",
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "撤回已发送的机器人 DING",
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.recall_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"需要以用户身份撤回 DING 时不要使用本命令"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"需要以用户身份撤回 DING 时不要使用本命令"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "user_required",
"source": "skills/mono/SKILL.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill danger table explicitly reviews this operation"
},
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"effect": {
"value": "write",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "write",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "destructive",
"source": "skills/mono/SKILL.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill danger table explicitly reviews this operation"
},
{
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": false
}
]
},
"examples": {
"value": [
"dws ding message recall --robot-code \u003cROBOT_CODE\u003e --id \u003cOPEN_DING_ID\u003e --format json"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws ding message recall --robot-code \u003cROBOT_CODE\u003e --id \u003cOPEN_DING_ID\u003e --format json"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.recall_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "ding.recall_ding_message",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"resolution": "highest_precedence",
"candidates": [
{
"value": "ding.recall_ding_message",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"selected": true
},
{
"value": "ding.recall_ding_message",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.recall_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "medium",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "medium",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "high",
"source": "skills/mono/SKILL.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill danger table explicitly reviews this operation"
},
{
"value": "medium",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"use_when": {
"value": [
"已知 openDingId 与同一 robot-code,需要撤回机器人 DING"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"已知 openDingId 与同一 robot-code,需要撤回机器人 DING"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "unknown",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "ding",
"rpc_name": "recall_ding_message"
},
"reviewed": true,
"risk": "medium",
"source_refs": [
"CommandRegistry:canonical_path=ding.recall_ding_message",
"Skill:skills/mono/references/products/ding.md",
"cobra-help:dws ding message recall",
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.ding.prod.json#ding.recall_ding_message",
"internal/cli/schema_command_registry.json#ding.recall_ding_message",
"internal/cli/schema_hints/imported/wukong.json",
"internal/cli/schema_hints/metadata/ding.json",
"internal/cli/schema_hints/selection/ding.json",
"internal/cli/schema_mcp_metadata.json#tools.ding.recall_ding_message",
"live-dws-schema:ding.recall_ding_message",
"skills/mono/SKILL.md",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/ding.md",
"structured-hint:internal/cli/schema_hints/imported/wukong.json#ding.recall_ding_message",
"structured-hint:internal/cli/schema_hints/selection-review.json#ding.recall_ding_message"
],
"use_when": [
"已知 openDingId 与同一 robot-code,需要撤回机器人 DING"
]
},
"ding message send": {
"agent_summary": "以企业机器人发送应用内/短信/电话 DING",
"agent_summary_source": "dws-agent-selection/ding",
"availability": "available",
"avoid_when": [
"普通聊天消息用 chat message send / send-by-bot",
"需要用户身份 DING 时不要用本命令(机器人身份)",
"短信/电话有成本,用户未确认前不要发 call/sms"
],
"confirmation": "not_required",
"effect": "write",
"effect_source": "command-verb",
"examples": [
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --users userId1,userId2 --content \"请查看\" --format json",
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --type call --users userId1 --content \"紧急告警\" --format json"
],
"field_provenance": {
"agent_summary": {
"value": "以企业机器人发送应用内/短信/电话 DING",
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "以企业机器人发送应用内/短信/电话 DING",
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.send_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"普通聊天消息用 chat message send / send-by-bot",
"需要用户身份 DING 时不要用本命令(机器人身份)",
"短信/电话有成本,用户未确认前不要发 call/sms"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"普通聊天消息用 chat message send / send-by-bot",
"需要用户身份 DING 时不要用本命令(机器人身份)",
"短信/电话有成本,用户未确认前不要发 call/sms"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --users userId1,userId2 --content \"请查看\" --format json",
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --type call --users userId1 --content \"紧急告警\" --format json"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --users userId1,userId2 --content \"请查看\" --format json",
"dws ding message send --robot-code \u003cROBOT_CODE\u003e --type call --users userId1 --content \"紧急告警\" --format json"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.send_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "ding.send_ding_message",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"resolution": "highest_precedence",
"candidates": [
{
"value": "ding.send_ding_message",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"selected": true
},
{
"value": "ding.send_ding_message",
"source": "internal/cli/schema_mcp_metadata.json#tools.ding.send_ding_message.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/ding marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "medium",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "medium",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"需要用企业机器人向指定 userId 发送应用内、短信或电话 DING"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"需要用企业机器人向指定 userId 发送应用内、短信或电话 DING"
],
"source": "internal/cli/schema_hints/selection/ding.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "unknown",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "ding",
"rpc_name": "send_ding_message"
},
"reviewed": true,
"risk": "medium",
"source_refs": [
"CommandRegistry:canonical_path=ding.send_ding_message",
"Skill:skills/mono/references/products/ding.md",
"cobra-help:dws ding message send",
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.ding.prod.json#ding.send_ding_message",
"internal/cli/schema_command_registry.json#ding.send_ding_message",
"internal/cli/schema_hints/imported/wukong.json",
"internal/cli/schema_hints/metadata/ding.json",
"internal/cli/schema_hints/selection/ding.json",
"internal/cli/schema_mcp_metadata.json#tools.ding.send_ding_message",
"live-dws-schema:ding.send_ding_message",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/ding.md",
"structured-hint:internal/cli/schema_hints/imported/wukong.json#ding.send_ding_message",
"structured-hint:internal/cli/schema_hints/selection-review.json#ding.send_ding_message"
],
"use_when": [
"需要用企业机器人向指定 userId 发送应用内、短信或电话 DING"
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,266 @@
{
"product_id": "live",
"tools": {
"live stream list": {
"agent_summary": "查看当前用户发起的直播列表与基础统计",
"agent_summary_source": "dws-agent-selection/live",
"availability": "available",
"avoid_when": [
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws live stream list"
],
"field_provenance": {
"agent_summary": {
"value": "查看当前用户发起的直播列表与基础统计",
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "查看当前用户发起的直播列表与基础统计",
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/live marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws live stream list"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws live stream list"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/live marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "live.get_my_lives",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"resolution": "highest_precedence",
"candidates": [
{
"value": "live.get_my_lives",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"selected": true
},
{
"value": "live.get_my_lives",
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/live marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"用户要看自己发起过的直播、状态或观看量等列表信息"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"用户要看自己发起过的直播、状态或观看量等列表信息"
],
"source": "internal/cli/schema_hints/selection/live.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "live",
"rpc_name": "get_my_lives"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"CommandRegistry:canonical_path=live.get_my_lives",
"Skill:skills/multi/dingtalk-live/SKILL.md",
"cobra-help:dws live stream list",
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.live.prod.json#live.get_my_lives",
"internal/cli/schema_command_registry.json#live.get_my_lives",
"internal/cli/schema_hints/imported/wukong.json",
"internal/cli/schema_hints/metadata/live.json",
"internal/cli/schema_hints/selection/live.json",
"internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives",
"live-dws-schema:live.get_my_lives",
"skills/mono/references/products/live.md",
"structured-hint:internal/cli/schema_hints/imported/wukong.json#live.get_my_lives",
"structured-hint:internal/cli/schema_hints/selection-review.json#live.get_my_lives"
],
"use_when": [
"用户要看自己发起过的直播、状态或观看量等列表信息"
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+582
View File
@@ -0,0 +1,582 @@
{
"product_id": "pat",
"tools": {
"pat browser-policy": {
"agent_summary": "配置 PAT 授权流程是否允许打开本地浏览器",
"agent_summary_source": "dws-agent-selection/pat",
"availability": "available",
"avoid_when": [
"需要授予产品 scope 时用 pat chmod,而不是改浏览器策略"
],
"confirmation": "not_required",
"effect": "write",
"effect_source": "agent-hint",
"examples": [
"dws pat browser-policy --enabled --format json",
"dws pat browser-policy --enabled=false --agentCode \u003cAGENT_CODE\u003e --format json"
],
"field_provenance": {
"agent_summary": {
"value": "配置 PAT 授权流程是否允许打开本地浏览器",
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "配置 PAT 授权流程是否允许打开本地浏览器",
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
}
]
},
"avoid_when": {
"value": [
"需要授予产品 scope 时用 pat chmod,而不是改浏览器策略"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"需要授予产品 scope 时用 pat chmod,而不是改浏览器策略"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "write",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "write",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": false
}
]
},
"examples": {
"value": [
"dws pat browser-policy --enabled --format json",
"dws pat browser-policy --enabled=false --agentCode \u003cAGENT_CODE\u003e --format json"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws pat browser-policy --enabled --format json",
"dws pat browser-policy --enabled=false --agentCode \u003cAGENT_CODE\u003e --format json"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "local",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "local",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
}
]
},
"interface_reason": {
"value": "命令仅操作本地进程或策略文件,不调用 MCP 接口",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "命令仅操作本地进程或策略文件,不调用 MCP 接口",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode local forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode local forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "medium",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "medium",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "medium",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"use_when": {
"value": [
"需要允许或禁止某 Agent 在 PAT 授权时打开浏览器"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"需要允许或禁止某 Agent 在 PAT 授权时打开浏览器"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "unknown",
"interface_mode": "local",
"interface_reason": "命令仅操作本地进程或策略文件,不调用 MCP 接口",
"reviewed": true,
"risk": "medium",
"source_refs": [
"CommandRegistry:canonical_path=pat.browser_policy",
"Skill:skills/mono/references/products/pat.md",
"cobra-help:dws pat browser-policy",
"internal/cli/schema_command_registry.json#pat.browser_policy",
"internal/cli/schema_hints/metadata/pat.json",
"internal/cli/schema_hints/selection/pat.json",
"live-dws-schema:pat.browser_policy#FAILED",
"skills/mono/references/products/pat.md",
"structured-hint:internal/cli/schema_hints/selection-review.json#pat.browser_policy"
],
"use_when": [
"需要允许或禁止某 Agent 在 PAT 授权时打开浏览器"
]
},
"pat chmod": {
"agent_summary": "预览或执行 PAT 批量行为授权(支持 dryRun / pending flow)",
"agent_summary_source": "dws-agent-selection/pat",
"availability": "available",
"avoid_when": [
"普通 OAuth 登录用 auth,不要用 pat",
"授权产品、grant-type 或 session-id 未明确时不要执行写入",
"只要改本地浏览器打开策略时用 pat browser-policy"
],
"confirmation": "user_required",
"effect": "write",
"effect_source": "agent-hint",
"examples": [
"dws pat chmod --products calendar,aitable --grant-type session --session-id \u003cSESSION_ID\u003e --dry-run --format json"
],
"field_provenance": {
"agent_summary": {
"value": "预览或执行 PAT 批量行为授权(支持 dryRun / pending flow)",
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": "预览或执行 PAT 批量行为授权(支持 dryRun / pending flow)",
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.pat.batch_grant.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"普通 OAuth 登录用 auth,不要用 pat",
"授权产品、grant-type 或 session-id 未明确时不要执行写入",
"只要改本地浏览器打开策略时用 pat browser-policy"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"普通 OAuth 登录用 auth,不要用 pat",
"授权产品、grant-type 或 session-id 未明确时不要执行写入",
"只要改本地浏览器打开策略时用 pat browser-policy"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"confirmation": {
"value": "user_required",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "user_required",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "user_required",
"source": "skills/mono/references/products/pat.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill command comment explicitly marks the operation as risky"
}
]
},
"effect": {
"value": "write",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "write",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "write",
"source": "skills/mono/references/products/pat.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill command comment explicitly marks the operation as risky"
},
{
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": false
}
]
},
"examples": {
"value": [
"dws pat chmod --products calendar,aitable --grant-type session --session-id \u003cSESSION_ID\u003e --dry-run --format json"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"dws pat chmod --products calendar,aitable --grant-type session --session-id \u003cSESSION_ID\u003e --dry-run --format json"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"idempotency": {
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.pat.batch_grant.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "pat.pat.batch_grant",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "pat.pat.batch_grant",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "pat.pat.batch_grant",
"source": "internal/cli/schema_mcp_metadata.json#tools.pat.batch_grant.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
},
"risk": {
"value": "high",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed",
"candidates": [
{
"value": "high",
"source": "internal/cli/schema_hints/metadata/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/pat marks this tool as reviewed"
},
{
"value": "high",
"source": "skills/mono/references/products/pat.md",
"precedence": "skill",
"selected": false,
"review_reason": "Skill command comment explicitly marks the operation as risky"
}
]
},
"use_when": {
"value": [
"命令提示缺少行为授权,需要按产品或 scope 批量授权",
"先 --dry-run 查看 selected/skipped/pending,用户确认后再执行"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
"candidates": [
{
"value": [
"命令提示缺少行为授权,需要按产品或 scope 批量授权",
"先 --dry-run 查看 selected/skipped/pending,用户确认后再执行"
],
"source": "internal/cli/schema_hints/selection/pat.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。"
}
]
}
},
"idempotency": "unknown",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "pat",
"rpc_name": "pat.batch_grant"
},
"reviewed": true,
"risk": "high",
"source_refs": [
"CommandRegistry:canonical_path=pat.batch_grant",
"Skill:skills/mono/references/products/pat.md",
"cobra-help:dws pat chmod",
"helper:internal/pat/chmod.go#typed_yes",
"internal/cli/schema_command_registry.json#pat.batch_grant",
"internal/cli/schema_hints/metadata/pat.json",
"internal/cli/schema_hints/selection/pat.json",
"internal/cli/schema_mcp_metadata.json#tools.pat.batch_grant",
"live-dws-schema:pat.batch_grant#FAILED",
"pinned-mcp:pat.batch_grant",
"skills/mono/references/products/pat.md",
"structured-hint:internal/cli/schema_hints/selection-review.json#pat.batch_grant"
],
"use_when": [
"命令提示缺少行为授权,需要按产品或 scope 批量授权",
"先 --dry-run 查看 selected/skipped/pending,用户确认后再执行"
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+593
View File
@@ -0,0 +1,593 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"encoding/json"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestEmbeddedAgentMetadataLoadsSplitDomains(t *testing.T) {
metadata := loadEmbeddedAgentMetadata()
if len(metadata.Domains) < 2 {
t.Fatalf("domains = %#v, want split product metadata", metadata.Domains)
}
if len(metadata.Tools) != metadata.Coverage.ToolsWithMetadata {
t.Fatalf("tools = %d, coverage = %#v", len(metadata.Tools), metadata.Coverage)
}
if _, ok := metadata.Tools["calendar event create"]; !ok {
t.Fatalf("calendar domain did not load: %#v", metadata.Domains)
}
coverage := metadata.Coverage
if coverage.ToolsWithUseWhen != len(metadata.Tools) ||
coverage.ToolsWithAvoidWhen != len(metadata.Tools) ||
coverage.ToolsWithExamples != len(metadata.Tools) ||
coverage.ToolsWithInterfaceMode != len(metadata.Tools) {
t.Fatalf("selection metadata coverage = %#v, tools=%d", coverage, len(metadata.Tools))
}
for path, tool := range metadata.Tools {
if len(tool.UseWhen) == 0 || len(tool.AvoidWhen) == 0 || len(tool.Examples) == 0 {
t.Errorf("tool %s has incomplete selection metadata: %#v", path, tool)
}
if tool.InterfaceMode == "" || tool.Availability == "" {
t.Errorf("tool %s has incomplete interface disposition: %#v", path, tool)
}
for _, example := range tool.Examples {
if strings.Contains(" "+example+" ", " --yes ") {
t.Errorf("tool %s example bypasses confirmation: %q", path, example)
}
}
}
}
func TestAgentMetadataTypedAccessorRoundTripsProvenance(t *testing.T) {
const encoded = `{
"product_id": "calendar",
"tools": {
"calendar attendee update": {
"agent_summary": "Update one attendee",
"agent_summary_source": "reviewed-selection",
"use_when": ["change an attendee"],
"avoid_when": ["read attendees"],
"prerequisites": ["event id"],
"tips": ["verify the attendee id"],
"effect": "write",
"effect_source": "agent-hint",
"risk": "low",
"confirmation": "not_required",
"idempotency": "non_idempotent",
"workflow_refs": ["calendar-update"],
"examples": ["dws calendar attendee update --event-id e1"],
"reviewed": true,
"source_refs": ["internal/cli/schema_hints/calendar.json"],
"interface_ref": {"product_id": "calendar", "rpc_name": "update_attendee"},
"interface_mode": "mcp",
"availability": "available",
"interface_reason": "reviewed RPC mapping",
"field_provenance": {
"risk": {
"value": "low",
"source": "reviewed.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "reviewed downgrade",
"candidates": [
{"value": "low", "source": "reviewed.json", "precedence": "reviewed_explicit", "review_reason": "reviewed downgrade", "selected": true},
{"value": "high", "source": "imported.json", "precedence": "imported", "selected": false}
],
"overridden_candidates": [
{"value": "medium", "source": "generated-default", "precedence": "inference_or_default", "selected": false}
]
}
}
}
}
}`
var fragment embeddedAgentMetadataDomain
if err := json.Unmarshal([]byte(encoded), &fragment); err != nil {
t.Fatalf("decode generated Agent metadata: %v", err)
}
roundTrip, err := json.Marshal(fragment)
if err != nil {
t.Fatalf("encode generated Agent metadata: %v", err)
}
var decoded embeddedAgentMetadataDomain
if err := json.Unmarshal(roundTrip, &decoded); err != nil {
t.Fatalf("round-trip generated Agent metadata: %v", err)
}
metadataFixture := embeddedAgentMetadata{
Products: map[string]agentProductMetadata{},
Tools: decoded.Tools,
}
safety, interfaceSpec, selection, provenance, ok := agentToolContractForPathsFromMetadata(metadataFixture, "missing", " calendar attendee update ")
if !ok {
t.Fatal("typed Agent metadata lookup failed")
}
if safety != (SafetySpec{Effect: "write", EffectSource: "agent-hint", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent"}) {
t.Fatalf("safety = %#v", safety)
}
if interfaceSpec.Ref == nil || interfaceSpec.Ref.ProductID != "calendar" || interfaceSpec.Ref.RPCName != "update_attendee" || interfaceSpec.Mode != "mcp" || interfaceSpec.Availability != "available" || interfaceSpec.Reason != "reviewed RPC mapping" {
t.Fatalf("interface = %#v", interfaceSpec)
}
if selection.AgentSummary != "Update one attendee" || selection.MetadataSource != embeddedAgentMetadataSource || selection.Reviewed == nil || !*selection.Reviewed || len(selection.Examples) != 1 {
t.Fatalf("selection = %#v", selection)
}
risk := provenance["risk"]
if string(risk.Value) != `"low"` || risk.Source != "reviewed.json" || risk.Precedence != "reviewed_explicit" || risk.Resolution != "highest_precedence" || risk.ReviewReason != "reviewed downgrade" {
t.Fatalf("risk provenance = %#v", risk)
}
if len(risk.Candidates) != 2 || risk.Candidates[0].Selected == nil || !*risk.Candidates[0].Selected || risk.Candidates[1].Selected == nil || *risk.Candidates[1].Selected {
t.Fatalf("risk candidates = %#v", risk.Candidates)
}
if string(risk.Candidates[1].Value) != `"high"` || risk.Candidates[1].Source != "imported.json" {
t.Fatalf("overridden risk candidate = %#v", risk.Candidates[1])
}
if len(risk.OverriddenCandidates) != 1 || string(risk.OverriddenCandidates[0].Value) != `"medium"` || risk.OverriddenCandidates[0].Precedence != "inference_or_default" {
t.Fatalf("legacy overridden candidates were dropped: %#v", risk.OverriddenCandidates)
}
// Accessors return detached typed values; callers cannot mutate the
// embedded snapshot and accidentally change a later schema response.
interfaceSpec.Ref.ProductID = "mutated"
selection.UseWhen[0] = "mutated"
risk.Candidates[0].Value[0] = 'x'
provenance["risk"] = risk
_, interfaceAgain, selectionAgain, provenanceAgain, _ := agentToolContractForPathsFromMetadata(metadataFixture, "calendar attendee update")
if interfaceAgain.Ref.ProductID != "calendar" || selectionAgain.UseWhen[0] != "change an attendee" || string(provenanceAgain["risk"].Candidates[0].Value) != `"low"` {
t.Fatalf("typed accessor leaked mutable state: interface=%#v selection=%#v provenance=%#v", interfaceAgain, selectionAgain, provenanceAgain)
}
}
func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) {
selected := true
legacyRef := func(value string) FieldProvenance {
raw, _ := json.Marshal(value)
return FieldProvenance{
Value: raw,
Source: "agent-metadata.json",
Precedence: "explicit",
Resolution: "highest_precedence",
Candidates: []FieldCandidateProvenance{{
Value: append(json.RawMessage(nil), raw...),
Source: "agent-metadata.json",
Precedence: "explicit",
Selected: &selected,
}},
}
}
mode := resolvedFieldProvenance("local", "reviewed.json", "", "reviewed_explicit", "highest_precedence", "reviewed local wrapper")
metadataFixture := embeddedAgentMetadata{
Products: map[string]agentProductMetadata{},
Tools: map[string]agentToolMetadata{
"calendar event get": {
InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "calendar", RPCName: "get_event"},
InterfaceMode: "mcp",
Availability: "available",
FieldProvenance: map[string]FieldProvenance{
"interface_ref": legacyRef("calendar.get_event"),
},
},
"calendar helper run": {
InterfaceMode: "local",
Availability: "available",
InterfaceReason: "reviewed local wrapper",
FieldProvenance: map[string]FieldProvenance{
"interface_ref": legacyRef("<none>"),
"interface_mode": mode,
},
},
"calendar helper inspect": {
InterfaceMode: "local",
Availability: "available",
FieldProvenance: map[string]FieldProvenance{
"interface_mode": mode,
},
},
},
}
_, mcpInterface, _, mcpProvenance, ok := agentToolContractForPathsFromMetadata(metadataFixture, "calendar event get")
if !ok {
t.Fatal("mcp metadata lookup failed")
}
wantRef := `{"product_id":"calendar","rpc_name":"get_event"}`
if got := string(mcpProvenance["interface_ref"].Value); got != wantRef {
t.Fatalf("typed interface_ref winner = %s, want %s", got, wantRef)
}
if got := string(mcpProvenance["interface_ref"].Candidates[0].Value); got != wantRef {
t.Fatalf("typed interface_ref candidate = %s, want %s", got, wantRef)
}
if err := validateFinalFieldProvenance("calendar.event_get", "interface_ref", mcpProvenance["interface_ref"], mcpInterface.Ref); err != nil {
t.Fatalf("typed mcp provenance = %v", err)
}
for _, field := range []string{"interface_reason", "agent_summary"} {
if _, exists := mcpProvenance[field]; exists {
t.Fatalf("typed adapter invented absent %s provenance: %#v", field, mcpProvenance[field])
}
}
_, localInterface, _, provenance, ok := agentToolContractForPathsFromMetadata(metadataFixture, "calendar helper run")
if !ok {
t.Fatal("calendar helper run metadata lookup failed")
}
if got := string(provenance["interface_ref"].Value); got != "null" {
t.Fatalf("calendar helper run interface_ref winner = %s, want null", got)
}
if err := validateFinalFieldProvenance("calendar helper run", "interface_ref", provenance["interface_ref"], localInterface.Ref); err != nil {
t.Fatalf("calendar helper run typed null provenance = %v", err)
}
_, _, _, provenance, ok = agentToolContractForPathsFromMetadata(metadataFixture, "calendar helper inspect")
if !ok {
t.Fatal("calendar helper inspect metadata lookup failed")
}
if _, exists := provenance["interface_ref"]; exists {
t.Fatalf("typed adapter repaired missing interface_ref provenance: %#v", provenance["interface_ref"])
}
}
func TestAgentMetadataTypedAdapterDoesNotLaunderInterfaceConflict(t *testing.T) {
selected := true
wrong, _ := json.Marshal("calendar.wrong_rpc")
provenance := FieldProvenance{
Value: wrong,
Source: "bad.json",
Precedence: "explicit",
Resolution: "highest_precedence",
Candidates: []FieldCandidateProvenance{{
Value: wrong, Source: "bad.json", Precedence: "explicit", Selected: &selected,
}},
}
projected := projectAgentInterfaceRefProvenance(provenance, &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"})
if string(projected.Value) != string(wrong) {
t.Fatalf("conflicting winner was rewritten: %s", projected.Value)
}
if err := validateFinalFieldProvenance("calendar.event_get", "interface_ref", projected, &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}); err == nil {
t.Fatal("conflicting interface_ref provenance unexpectedly validated")
}
}
func TestAgentProductSelectionUsesTypedAccessor(t *testing.T) {
provenance := resolvedFieldProvenance("Document operations", "manual", "manual.json", "reviewed_manual", "highest_precedence", "reviewed")
metadataFixture := embeddedAgentMetadata{
Products: map[string]agentProductMetadata{
"doc": {
AgentSummary: "Document operations",
AgentSummarySource: "reviewed-doc-routing",
UseWhen: []string{"create a document", "create a document"},
AvoidWhen: []string{"manage a spreadsheet"},
SourceRefs: []string{"z.md", "a.md"},
FieldProvenance: map[string]FieldProvenance{"agent_summary": provenance},
},
},
Tools: map[string]agentToolMetadata{},
}
selection, ok := agentProductSelectionForIDsFromMetadata(metadataFixture, "missing", " doc ")
if !ok || selection.AgentSummary != "Document operations" || selection.AgentSummarySource != "reviewed-doc-routing" || selection.MetadataSource != embeddedAgentMetadataSource {
t.Fatalf("product selection = %#v, ok=%v", selection, ok)
}
if len(selection.UseWhen) != 1 || len(selection.SourceRefs) != 2 || selection.SourceRefs[0] != "a.md" {
t.Fatalf("normalized product selection = %#v", selection)
}
_, deliveredProvenance, ok := agentProductContractForIDsFromMetadata(metadataFixture, "doc")
if !ok || string(deliveredProvenance["agent_summary"].Value) != `"Document operations"` {
t.Fatalf("product provenance = %#v, ok=%v", deliveredProvenance, ok)
}
selection.UseWhen[0] = "mutated"
deliveredProvenance["agent_summary"] = FieldProvenance{}
again, _ := agentProductSelectionForIDsFromMetadata(metadataFixture, "doc")
if again.UseWhen[0] != "create a document" {
t.Fatalf("product accessor leaked mutable state: %#v", again)
}
_, againProvenance, _ := agentProductContractForIDsFromMetadata(metadataFixture, "doc")
if len(againProvenance["agent_summary"].Value) == 0 {
t.Fatal("product accessor leaked mutable provenance state")
}
}
func TestRuntimeSchemaIncludesEmbeddedAgentMetadata(t *testing.T) {
agentFixture := embeddedAgentMetadata{
Version: 1,
SourceHash: "sha256:test",
Products: map[string]agentProductMetadata{
"doc": {
AgentSummary: "创建、读取和维护钉钉文档",
AgentSummarySource: "test-source",
UseWhen: []string{"需要创建或读取文档"},
SourceRefs: []string{"skills/mono/SKILL.md"},
},
},
Tools: map[string]agentToolMetadata{
"doc create": {
UseWhen: []string{"新建文档"},
AvoidWhen: []string{"只需读取文档时"},
Effect: "write",
EffectSource: "command-verb",
Examples: []string{"dws doc create --title test"},
SourceRefs: []string{"skills/mono/references/products/doc.md"},
InterfaceMode: "local",
Availability: "available",
InterfaceReason: "test local implementation",
},
},
}
mcpFixture := embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{}}
root := buildRuntimeSchemaTestRoot()
leaf, err := runtimeSchemaPayloadForTestWithMetadata(root, []string{"doc.create_document"}, agentFixture, mcpFixture)
if err != nil {
t.Fatalf("runtimeSchemaPayloadForTest(leaf): %v", err)
}
if leaf["effect"] != "write" || leaf["agent_metadata_source"] != embeddedAgentMetadataSource {
t.Fatalf("leaf Agent metadata = %#v", leaf)
}
if leaf["interface_mode"] != "local" || leaf["availability"] != "available" || leaf["interface_reason"] != "test local implementation" {
t.Fatalf("leaf interface disposition = %#v", leaf)
}
if examples, _ := leaf["examples"].([]string); len(examples) != 1 {
t.Fatalf("leaf examples = %#v", leaf["examples"])
}
catalog, err := runtimeSchemaPayloadForTestWithMetadata(root, nil, agentFixture, mcpFixture)
if err != nil {
t.Fatalf("runtimeSchemaPayloadForTest(catalog): %v", err)
}
summary, _ := catalog["agent_metadata"].(map[string]any)
if summary["source_hash"] != "sha256:test" {
t.Fatalf("catalog Agent metadata summary = %#v", summary)
}
products, _ := catalog["products"].([]map[string]any)
doc := findSchemaProduct(products, "doc")
if useWhen, _ := doc["use_when"].([]string); len(useWhen) != 1 {
t.Fatalf("doc product use_when = %#v", doc["use_when"])
}
tools, _ := doc["tools"].([]map[string]any)
if len(tools) != 1 || tools[0]["effect"] != "write" {
t.Fatalf("doc tool summaries = %#v", tools)
}
if _, exists := tools[0]["examples"]; exists {
t.Fatalf("product summary must not include examples: %#v", tools[0])
}
registry, err := schemaRegistryForTestWithMetadata(root, agentFixture, mcpFixture)
if err != nil {
t.Fatalf("schemaRegistryForTest(): %v", err)
}
compact, err := registry.ToOverviewPayload()
if err != nil {
t.Fatalf("ToOverviewPayload(): %v", err)
}
compactProducts, _ := compact["products"].([]map[string]any)
compactDoc := findSchemaProduct(compactProducts, "doc")
if compactDoc["agent_summary"] != "创建、读取和维护钉钉文档" {
t.Fatalf("compact product summary = %#v", compactDoc)
}
if _, exists := compactDoc["agent_source_refs"]; exists {
t.Fatalf("compact product must omit provenance: %#v", compactDoc)
}
if _, exists := compactDoc["use_when"]; exists {
t.Fatalf("compact product with summary must omit routing expansion: %#v", compactDoc)
}
}
func TestRuntimeSchemaAllPayloadContainsFullLeafParameters(t *testing.T) {
payload, err := runtimeSchemaAllPayloadForTest(buildRuntimeSchemaTestRoot())
if err != nil {
t.Fatal(err)
}
products := schemaMapSlice(payload["products"])
doc := findSchemaProduct(products, "doc")
tools := schemaMapSlice(doc["tools"])
if len(tools) != 1 {
t.Fatalf("runtime full export tools = %#v", tools)
}
if got := schemaString(tools[0]["canonical_path"]); got != "doc.create_document" {
t.Fatalf("canonical path = %q", got)
}
parameters, ok := tools[0]["parameters"].(map[string]any)
if !ok || parameters["title"] == nil {
t.Fatalf("runtime full export parameters = %#v", tools[0]["parameters"])
}
}
func TestRuntimeSchemaReportsEmbeddedInterfaceMetadata(t *testing.T) {
mcpFixture := embeddedMCPMetadata{
Version: 1,
Source: "cli-registry",
SourceRevision: "revision-test",
SourceHash: "sha256:interface-test",
Coverage: embeddedMCPMetadataCoverage{
SurfaceScope: "source_revision",
SourceTools: 10,
SurfaceTools: 2,
MatchedTools: 1,
UnmatchedTools: 1,
},
Tools: map[string]embeddedMCPToolMetadata{
"doc.create_document": {Description: "创建文档"},
},
}
agentFixture := emptyEmbeddedAgentMetadata()
catalog, err := runtimeSchemaPayloadForTestWithMetadata(buildRuntimeSchemaTestRoot(), nil, agentFixture, mcpFixture)
if err != nil {
t.Fatalf("runtimeSchemaPayloadForTest(catalog): %v", err)
}
summary, _ := catalog["interface_metadata"].(map[string]any)
if summary["source"] != "cli-registry" || summary["source_hash"] != "sha256:interface-test" || schemaTestInt(summary["tool_count"]) != 1 {
t.Fatalf("interface metadata summary = %#v", summary)
}
coverage, _ := summary["coverage"].(map[string]any)
if summary["source_revision"] != "revision-test" || coverage["surface_scope"] != "source_revision" || schemaTestInt(coverage["surface_tools"]) != 2 {
t.Fatalf("interface metadata provenance = %#v", summary)
}
registry, err := schemaRegistryForTestWithMetadata(buildRuntimeSchemaTestRoot(), agentFixture, mcpFixture)
if err != nil {
t.Fatalf("schemaRegistryForTest(): %v", err)
}
compact, err := registry.ToOverviewPayload()
if err != nil {
t.Fatalf("ToOverviewPayload(): %v", err)
}
if compact["interface_metadata"] == nil {
t.Fatalf("compact schema dropped interface metadata: %#v", compact)
}
}
func schemaTestInt(value any) int {
switch typed := value.(type) {
case int:
return typed
case float64:
return int(typed)
case json.Number:
parsed, _ := typed.Int64()
return int(parsed)
case string:
parsed, _ := json.Number(typed).Int64()
return int(parsed)
default:
return 0
}
}
func TestRuntimeSchemaUsesVersionedInterfaceRef(t *testing.T) {
agentFixture := embeddedAgentMetadata{
Tools: map[string]agentToolMetadata{
"doc create": {
InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "documents", RPCName: "create_doc_v2"},
InterfaceMode: "mcp",
Availability: "available",
},
},
Products: map[string]agentProductMetadata{},
}
mcpFixture := embeddedMCPMetadata{
Tools: map[string]embeddedMCPToolMetadata{
"documents.create_doc_v2": {
Parameters: map[string]embeddedMCPParamMeta{
"title": {Description: "MCP document title"},
},
},
},
}
payload, err := runtimeSchemaPayloadForTestWithMetadata(buildRuntimeSchemaTestRoot(), []string{"doc.create_document"}, agentFixture, mcpFixture)
if err != nil {
t.Fatal(err)
}
ref, _ := payload["interface_ref"].(map[string]any)
if ref["product_id"] != "documents" || ref["rpc_name"] != "create_doc_v2" {
t.Fatalf("interface_ref = %#v", payload["interface_ref"])
}
parameters, _ := payload["parameters"].(map[string]any)
title, _ := parameters["title"].(map[string]any)
if title["interface_description"] != "MCP document title" {
t.Fatalf("title metadata = %#v", title)
}
}
func TestMCPRequiredParticipatesInSourcePrecedence(t *testing.T) {
required := true
agentFixture := emptyEmbeddedAgentMetadata()
mcpFixture := embeddedMCPMetadata{
Tools: map[string]embeddedMCPToolMetadata{
"sample.list_items": {
Parameters: map[string]embeddedMCPParamMeta{
"limit": {Required: &required},
},
},
},
}
root := &cobra.Command{Use: "dws"}
list := &cobra.Command{Use: "list", Run: func(*cobra.Command, []string) {}}
list.Flags().Int("limit", 0, "optional page size")
AttachRuntimeSchema(list, "sample", "list_items", "test")
sample := &cobra.Command{Use: "sample"}
sample.AddCommand(list)
root.AddCommand(sample)
payload, err := runtimeSchemaPayloadForTestWithMetadata(root, []string{"sample.list_items"}, agentFixture, mcpFixture)
if err != nil {
t.Fatal(err)
}
parameters, _ := payload["parameters"].(map[string]any)
limit, _ := parameters["limit"].(map[string]any)
if limit["required"] != true {
t.Fatalf("MCP required candidate did not win over the default: %#v", limit)
}
}
func TestMCPDefaultDoesNotOverrideCLIDefault(t *testing.T) {
agentFixture := emptyEmbeddedAgentMetadata()
mcpFixture := embeddedMCPMetadata{
Tools: map[string]embeddedMCPToolMetadata{
"sample.list_items": {
Parameters: map[string]embeddedMCPParamMeta{
"limit": {Default: "50"},
},
},
},
}
root := &cobra.Command{Use: "dws"}
list := &cobra.Command{Use: "list", Run: func(*cobra.Command, []string) {}}
list.Flags().Int("limit", 10, "optional page size")
AttachRuntimeSchema(list, "sample", "list_items", "test")
sample := &cobra.Command{Use: "sample"}
sample.AddCommand(list)
root.AddCommand(sample)
payload, err := runtimeSchemaPayloadForTestWithMetadata(root, []string{"sample.list_items"}, agentFixture, mcpFixture)
if err != nil {
t.Fatal(err)
}
parameters, _ := payload["parameters"].(map[string]any)
limit, _ := parameters["limit"].(map[string]any)
if limit["default"] != "10" || limit["interface_default"] != "50" {
t.Fatalf("CLI and interface defaults were not separated: %#v", limit)
}
}
func findSchemaProduct(products []map[string]any, id string) map[string]any {
for _, product := range products {
if product["id"] == id {
return product
}
}
return nil
}
func buildRuntimeSchemaTestRoot() *cobra.Command {
root := &cobra.Command{Use: "dws"}
create := &cobra.Command{Use: "create", Short: "Create document", Run: func(*cobra.Command, []string) {}}
create.Flags().String("title", "", "Document title")
AttachRuntimeSchema(create, "doc", "create_document", "runtime:doc")
AnnotateRuntimeFlag(create, "title", "title", "string", true, "")
doc := &cobra.Command{Use: "doc", Short: "Docs"}
doc.AddCommand(create)
root.AddCommand(doc)
return root
}
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
)
const manualAgentSelectionFixtureVersion = 1
// ManualAgentSelectionCase is one reproducible model-evaluation assertion
// derived from reviewed Manual Agent hints. Positive cases require one exact
// canonical result; negative cases only forbid the command that owns the
// avoid_when text. CandidateCanonicals contains every bound tool in the same
// product, in stable order.
type ManualAgentSelectionCase struct {
ID string `json:"id"`
ProductID string `json:"product_id"`
Scenario string `json:"scenario"`
ExpectedCanonical string `json:"expected_canonical,omitempty"`
ForbiddenCanonical string `json:"forbidden_canonical,omitempty"`
CandidateCanonicals []string `json:"candidate_canonicals"`
}
// ManualAgentSelectionFixture is the stable input to an optional live Agent
// evaluation. It is built from schema_hints/selection and the real bound
// command tree; it is not a second authored hint source.
type ManualAgentSelectionFixture struct {
Version int `json:"version"`
Cases []ManualAgentSelectionCase `json:"cases"`
}
// ManualAgentSelectionReport records deterministic coverage and the exact
// fixture digest. It proves that all reviewed assertions are well-formed and
// executable; it deliberately does not claim that a language model understood
// their natural-language meaning.
type ManualAgentSelectionReport struct {
Tools int
PositiveAssertions int
NegativeAssertions int
FixtureSHA256 string
}
// BuildManualAgentSelectionEvalFixture turns every use_when and avoid_when
// entry into a typed, reproducible evaluation case and validates it against
// the exact BoundCommandRegistry.
//
// Deterministic CI can prove full coverage, real command binding, and absence
// of literal contradictory expectations. Semantic command choice belongs to
// an opt-in live-model evaluation that consumes this fixture; this function
// never substitutes string matching for Agent behavior.
func BuildManualAgentSelectionEvalFixture(bound BoundCommandRegistry, hints ManualAgentHintSet) (ManualAgentSelectionFixture, ManualAgentSelectionReport, error) {
fixture := ManualAgentSelectionFixture{Version: manualAgentSelectionFixtureVersion}
report := ManualAgentSelectionReport{}
expectedTools := make(map[string]bool, len(bound.Commands))
candidatesByProduct := map[string][]string{}
for _, command := range bound.Commands {
canonical := strings.TrimSpace(command.CanonicalPath)
expectedTools[canonical] = true
productID, _, ok := strings.Cut(canonical, ".")
if !ok || strings.TrimSpace(productID) == "" {
return fixture, report, fmt.Errorf("agent_hints selection has invalid bound canonical path %q", canonical)
}
candidatesByProduct[productID] = append(candidatesByProduct[productID], canonical)
}
if err := validateManualAgentHintExactSet("selection tools", expectedTools, mapKeysManualAgentTools(hints.Tools)); err != nil {
return fixture, report, err
}
for productID := range candidatesByProduct {
sort.Strings(candidatesByProduct[productID])
}
canonicals := make([]string, 0, len(expectedTools))
for canonical := range expectedTools {
canonicals = append(canonicals, canonical)
}
sort.Strings(canonicals)
positiveExpectations := map[string]string{}
positiveDisplays := map[string]string{}
for _, canonical := range canonicals {
command, ok := bound.ByCanonical[canonical]
if !ok {
return fixture, report, fmt.Errorf("agent_hints selection expected canonical %q is missing from BoundCommandRegistry.ByCanonical", canonical)
}
if err := validateManualAgentSelectionBinding(bound, canonical, command); err != nil {
return fixture, report, err
}
hint := hints.Tools[canonical]
if len(hint.UseWhen) == 0 {
return fixture, report, fmt.Errorf("agent_hints tool %s requires at least one positive use_when selection assertion", canonical)
}
if len(hint.AvoidWhen) == 0 {
return fixture, report, fmt.Errorf("agent_hints tool %s requires at least one negative avoid_when selection assertion", canonical)
}
productID, _, _ := strings.Cut(canonical, ".")
candidates := candidatesByProduct[productID]
for index, scenario := range hint.UseWhen {
normalized := normalizeManualAgentSelectionScenario(scenario)
if normalized == "" {
return fixture, report, fmt.Errorf("agent_hints tool %s has an empty normalized use_when selection assertion", canonical)
}
if previous, exists := positiveExpectations[normalized]; exists {
return fixture, report, fmt.Errorf("agent_hints use_when scenario %q has conflicting literal expectations %q and %q", positiveDisplays[normalized], previous, canonical)
}
positiveExpectations[normalized] = canonical
positiveDisplays[normalized] = strings.TrimSpace(scenario)
fixture.Cases = append(fixture.Cases, ManualAgentSelectionCase{
ID: fmt.Sprintf("%s/use_when/%d", canonical, index),
ProductID: productID,
Scenario: strings.TrimSpace(scenario),
ExpectedCanonical: canonical,
CandidateCanonicals: append([]string(nil), candidates...),
})
report.PositiveAssertions++
}
for index, scenario := range hint.AvoidWhen {
normalized := normalizeManualAgentSelectionScenario(scenario)
if normalized == "" {
return fixture, report, fmt.Errorf("agent_hints tool %s has an empty normalized avoid_when selection assertion", canonical)
}
if expected := positiveExpectations[normalized]; expected == canonical {
return fixture, report, fmt.Errorf("agent_hints tool %s has the same literal positive and negative selection scenario %q", canonical, strings.TrimSpace(scenario))
}
fixture.Cases = append(fixture.Cases, ManualAgentSelectionCase{
ID: fmt.Sprintf("%s/avoid_when/%d", canonical, index),
ProductID: productID,
Scenario: strings.TrimSpace(scenario),
ForbiddenCanonical: canonical,
CandidateCanonicals: append([]string(nil), candidates...),
})
report.NegativeAssertions++
}
}
report.Tools = len(canonicals)
digest, err := manualAgentSelectionFixtureDigest(fixture)
if err != nil {
return fixture, report, err
}
report.FixtureSHA256 = digest
return fixture, report, nil
}
// ValidateManualAgentSelectionContract is the lightweight generator-facing
// gate. Callers that run a semantic model evaluation should use
// BuildManualAgentSelectionEvalFixture and pass the returned cases to it.
func ValidateManualAgentSelectionContract(bound BoundCommandRegistry, hints ManualAgentHintSet) (ManualAgentSelectionReport, error) {
_, report, err := BuildManualAgentSelectionEvalFixture(bound, hints)
return report, err
}
func validateManualAgentSelectionBinding(bound BoundCommandRegistry, canonical string, command BoundCommandSpec) error {
if command.CanonicalPath != canonical {
return fmt.Errorf("agent_hints selection canonical %q resolves to mismatched BoundCommandRegistry entry %q", canonical, command.CanonicalPath)
}
if command.PrimaryCommand == nil {
return fmt.Errorf("agent_hints selection canonical %q has no bound primary Cobra command", canonical)
}
if !runnableSchemaLeaf(command.PrimaryCommand) {
return fmt.Errorf("agent_hints selection canonical %q primary path %q is not a runnable Cobra leaf", canonical, command.PrimaryCLIPath)
}
primaryPath := normalizeSchemaCLIPath(command.PrimaryCLIPath)
if primaryPath == "" {
return fmt.Errorf("agent_hints selection canonical %q has an empty primary CLI path", canonical)
}
byPath, ok := bound.ByCLIPath[primaryPath]
if !ok || byPath.CanonicalPath != canonical {
return fmt.Errorf("agent_hints selection canonical %q primary path %q is not bound back to the same tool", canonical, primaryPath)
}
return nil
}
func normalizeManualAgentSelectionScenario(value string) string {
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
}
func manualAgentSelectionFixtureDigest(fixture ManualAgentSelectionFixture) (string, error) {
data, err := json.Marshal(fixture)
if err != nil {
return "", fmt.Errorf("marshal Manual Agent selection fixture: %w", err)
}
digest := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(digest[:]), nil
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestBuildManualAgentSelectionEvalFixtureUsesReviewedScenariosAndRealCommands(t *testing.T) {
bound := manualAgentSelectionBoundFixture()
hints := manualAgentHintSetFixture()
fixture, report, err := BuildManualAgentSelectionEvalFixture(bound, hints)
if err != nil {
t.Fatalf("BuildManualAgentSelectionEvalFixture() error = %v", err)
}
if report.Tools != 1 || report.PositiveAssertions != 1 || report.NegativeAssertions != 1 {
t.Fatalf("selection report = %+v", report)
}
if report.FixtureSHA256 == "" || len(fixture.Cases) != 2 {
t.Fatalf("selection fixture = %+v, report = %+v", fixture, report)
}
positive, negative := fixture.Cases[0], fixture.Cases[1]
if positive.ExpectedCanonical != "sample.search_items" || positive.ForbiddenCanonical != "" {
t.Fatalf("positive case = %+v", positive)
}
if negative.ForbiddenCanonical != "sample.search_items" || negative.ExpectedCanonical != "" {
t.Fatalf("negative case = %+v", negative)
}
if !reflect.DeepEqual(positive.CandidateCanonicals, []string{"sample.search_items"}) {
t.Fatalf("positive candidates = %#v", positive.CandidateCanonicals)
}
_, repeated, err := BuildManualAgentSelectionEvalFixture(bound, hints)
if err != nil || repeated.FixtureSHA256 != report.FixtureSHA256 {
t.Fatalf("repeated fixture digest = %q, err=%v; want %q", repeated.FixtureSHA256, err, report.FixtureSHA256)
}
}
func TestBuildManualAgentSelectionEvalFixtureRejectsFactualContractDrift(t *testing.T) {
tests := []struct {
name string
mutate func(*BoundCommandRegistry, *ManualAgentHintSet)
wantErr string
}{
{
name: "missing reviewed tool",
mutate: func(_ *BoundCommandRegistry, hints *ManualAgentHintSet) {
delete(hints.Tools, "sample.search_items")
},
wantErr: "selection tools do not exactly match",
},
{
name: "unbound primary command",
mutate: func(bound *BoundCommandRegistry, _ *ManualAgentHintSet) {
item := bound.ByCanonical["sample.search_items"]
item.PrimaryCommand = nil
bound.ByCanonical["sample.search_items"] = item
},
wantErr: "no bound primary Cobra command",
},
{
name: "literal positive negative contradiction",
mutate: func(_ *BoundCommandRegistry, hints *ManualAgentHintSet) {
item := hints.Tools["sample.search_items"]
item.AvoidWhen = append([]string(nil), item.UseWhen...)
hints.Tools["sample.search_items"] = item
},
wantErr: "same literal positive and negative",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
bound := manualAgentSelectionBoundFixture()
hints := manualAgentHintSetFixture()
test.mutate(&bound, &hints)
_, _, err := BuildManualAgentSelectionEvalFixture(bound, hints)
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %v, want containing %q", err, test.wantErr)
}
})
}
}
func TestBuildManualAgentSelectionEvalFixtureRejectsOneScenarioWithTwoExpectedTools(t *testing.T) {
bound := manualAgentSelectionBoundFixture()
root := bound.ByCanonical["sample.search_items"].PrimaryCommand.Root()
create := &cobra.Command{Use: "create", RunE: func(*cobra.Command, []string) error { return nil }}
exactSchemaCommand(root, "sample item").AddCommand(create)
createSpec := BoundCommandSpec{
CommandSpec: CommandSpec{
CanonicalPath: "sample.create_item",
PrimaryCLIPath: "sample item create",
},
PrimaryCommand: create,
}
bound.Commands = append(bound.Commands, createSpec)
bound.ByCanonical[createSpec.CanonicalPath] = createSpec
bound.ByCLIPath[createSpec.PrimaryCLIPath] = createSpec
hints := manualAgentHintSetFixture()
search := hints.Tools["sample.search_items"]
createHint := search
createHint.AgentSummary = "Create a sample item"
createHint.Examples = []string{"dws sample item create"}
createHint.UseWhen = append([]string(nil), search.UseWhen...)
createHint.AvoidWhen = []string{"An existing sample item must be found"}
hints.Tools[createSpec.CanonicalPath] = createHint
_, _, err := BuildManualAgentSelectionEvalFixture(bound, hints)
if err == nil || !strings.Contains(err.Error(), "conflicting literal expectations") {
t.Fatalf("error = %v, want literal expectation conflict", err)
}
}
func manualAgentSelectionBoundFixture() BoundCommandRegistry {
_, leaf := manualSchemaHintTestTree()
spec := BoundCommandSpec{
CommandSpec: CommandSpec{
CanonicalPath: "sample.search_items",
PrimaryCLIPath: "sample item search",
},
PrimaryCommand: leaf,
}
return BoundCommandRegistry{
Commands: []BoundCommandSpec{spec},
ByCanonical: map[string]BoundCommandSpec{spec.CanonicalPath: spec},
ByCLIPath: map[string]BoundCommandSpec{spec.PrimaryCLIPath: spec},
}
}
+390
View File
@@ -0,0 +1,390 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"sync"
"sync/atomic"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
const SchemaCatalogSnapshotVersion = 1
//go:embed schema_catalog.json
var embeddedSchemaCatalogJSON []byte
// SchemaCatalogSnapshot is the release-stable Agent contract. Catalog holds
// the progressive product/tool index; Tools holds full leaf parameter schemas.
// It intentionally contains no endpoint, credential, or runtime cache data.
type SchemaCatalogSnapshot struct {
Version int `json:"version"`
SourceHash string `json:"source_hash"`
SurfaceHash string `json:"surface_hash,omitempty"`
Catalog map[string]any `json:"catalog"`
Tools map[string]map[string]any `json:"tools"`
}
// SchemaCatalogBuildOptions carries release-envelope inputs which are checked
// against the effective reviewed CommandRegistry. The command set is not an
// option: visibility is resolved by EffectiveCommandRegistry before assembly,
// and every public command must be delivered.
type SchemaCatalogBuildOptions struct {
RegistryHash string
}
type loadedSchemaCatalog struct {
Snapshot SchemaCatalogSnapshot
Registry SchemaRegistry
Index SchemaIndex
}
var (
runtimeEmbeddedSchemaCatalogOnce sync.Once
runtimeEmbeddedSchemaCatalog loadedSchemaCatalog
runtimeEmbeddedSchemaCatalogErr error
)
var runtimeEmbeddedSchemaCatalogLazyLoadCount atomic.Uint64
func embeddedSchemaCatalog() loadedSchemaCatalog {
runtimeEmbeddedSchemaCatalogOnce.Do(func() {
runtimeEmbeddedSchemaCatalogLazyLoadCount.Add(1)
runtimeEmbeddedSchemaCatalog, runtimeEmbeddedSchemaCatalogErr = decodeSchemaCatalogSnapshot(embeddedSchemaCatalogJSON)
})
return runtimeEmbeddedSchemaCatalog
}
func embeddedSchemaCatalogError() error {
_ = embeddedSchemaCatalog()
return runtimeEmbeddedSchemaCatalogErr
}
// BuildSchemaCatalogSnapshot renders a deterministic Catalog from one
// resolved source-to-delivery hand-off. It deliberately accepts no Cobra root:
// reapplying manual hints or rebuilding SchemaRegistry at this boundary would
// allow generation gates to validate one candidate while publishing another.
func BuildSchemaCatalogSnapshot(resolved ResolvedSchemaBuild, options SchemaCatalogBuildOptions) (SchemaCatalogSnapshot, error) {
if resolved.root == nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("schema Catalog source was not created by ResolveSchemaBuild")
}
registry := resolved.registry
effectiveCommands := resolved.effective
registryHash := strings.TrimSpace(options.RegistryHash)
if registryHash == "" {
registryHash = effectiveCommands.SourceHash()
} else if registryHash != effectiveCommands.SourceHash() {
return SchemaCatalogSnapshot{}, fmt.Errorf("provided Registry hash %q disagrees with effective CommandRegistry %q", registryHash, effectiveCommands.SourceHash())
}
if err := ValidateSchemaParameterBindingDelivery(resolved.bound, registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema parameter binding delivery: %w", err)
}
if err := ValidateReviewedDryRunCapabilityDelivery(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reviewed dry-run capability delivery: %w", err)
}
if _, err := ValidateEmbeddedManualAgentExampleDelivery(resolved.bound, registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Manual Agent example delivery: %w", err)
}
if err := validateResolvedRuntimeSchemaCompleteness(resolved.root, resolved.bound); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reverse command-tree completeness: %w", err)
}
if err := validateSchemaRegistryAgainstCommandRegistry(registry, effectiveCommands); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate typed Schema registry against reviewed CommandRegistry: %w", err)
}
if err := validateSchemaRegistryInterfaces(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
}
if err := validateSchemaRegistryAgentMetadata(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
}
if err := validateFinalSchemaProvenanceCoverage(registry); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema provenance: %w", err)
}
// Visibility has already selected the complete public set in
// collectRuntimeSchemaEntriesFromBound. Do not apply a post-assembly
// allowlist: doing so could silently erase an otherwise valid reviewed
// manual-only command after the exact-set validation above has passed.
registry.Source = "embedded-command-catalog"
payload, err := registry.ToSnapshotPayload()
if err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("serialize typed Schema registry: %w", err)
}
snapshot := SchemaCatalogSnapshot{
Version: SchemaCatalogSnapshotVersion,
SurfaceHash: registryHash,
Catalog: payload.Catalog,
Tools: payload.Tools,
}
snapshot.SourceHash = schemaCatalogSnapshotHash(snapshot)
if err := ValidateSchemaDeliveryInvariants(registry, snapshot); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema delivery invariants: %w", err)
}
if err := validateResolvedSchemaCatalogDeliveryCompleteness(resolved.root, resolved.bound, snapshot); err != nil {
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Catalog delivery completeness: %w", err)
}
return snapshot, nil
}
// decodeSchemaCatalogSnapshot is the single release and generation-time
// loading path. Delivery validation round-trips generated JSON through this
// function so it cannot pass with data that the shipped binary would reject.
func decodeSchemaCatalogSnapshot(data []byte) (loadedSchemaCatalog, error) {
var snapshot SchemaCatalogSnapshot
if err := decodeStrictSchemaJSON(data, &snapshot); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("decode Schema Catalog snapshot: %w", err)
}
return loadSchemaCatalogSnapshot(snapshot)
}
// loadSchemaCatalogSnapshot constructs the production lookup from an
// arbitrary decoded snapshot. It validates the progressive summaries against
// the full leaf store before publishing any lookup key.
func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCatalog, error) {
if snapshot.Version != SchemaCatalogSnapshotVersion {
return loadedSchemaCatalog{}, fmt.Errorf("unsupported Schema Catalog snapshot version %d", snapshot.Version)
}
if len(snapshot.Catalog) == 0 || len(snapshot.Tools) == 0 {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is empty")
}
if snapshot.SourceHash == "" || snapshot.SourceHash != schemaCatalogSnapshotHash(snapshot) {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot source_hash does not match its content")
}
registry, index, err := schemaRegistryFromSnapshot(snapshot)
if err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("load typed Schema registry: %w", err)
}
if err := validateSchemaRegistryInterfaces(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
}
// The production loader validates delivered provenance exactly as encoded.
// toolSpecFromSnapshot deliberately does not synthesize candidates or
// rewrite winners, and this coverage gate applies to every snapshot source.
if err := validateFinalSchemaProvenanceCoverage(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema provenance: %w", err)
}
if registry.Source == "embedded-command-catalog" {
if err := validateSchemaRegistryAgentMetadata(registry); err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
}
}
return loadedSchemaCatalog{Snapshot: snapshot, Registry: registry, Index: index}, nil
}
func embeddedSchemaCatalogAvailable() bool {
return len(embeddedSchemaCatalog().Index.CanonicalPaths()) > 0
}
func embeddedSchemaAllPayload() (map[string]any, error) {
loaded := embeddedSchemaCatalog()
payload, err := loaded.Registry.ToPayload()
if err != nil {
return nil, err
}
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
func embeddedSchemaOverviewPayload() (map[string]any, error) {
loaded := embeddedSchemaCatalog()
payload, err := loaded.Registry.ToOverviewPayload()
if err != nil {
return nil, err
}
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
func exactSchemaCommand(root *cobra.Command, rawPath string) *cobra.Command {
if root == nil {
return nil
}
parts := strings.Fields(strings.TrimSpace(rawPath))
if len(parts) > 0 && parts[0] == root.Name() {
parts = parts[1:]
}
current := root
for _, part := range parts {
var next *cobra.Command
for _, child := range current.Commands() {
if child.Name() == part {
next = child
break
}
}
if next == nil {
return nil
}
current = next
}
if current == root {
return nil
}
return current
}
func embeddedSchemaPayload(args []string) (map[string]any, error) {
return schemaPayloadFromLoadedCatalog(embeddedSchemaCatalog(), args)
}
// schemaPayloadFromLoadedCatalog is shared by the shipped schema command and
// the final-delivery gate. Keeping lookup and payload rendering on one path
// prevents generation-only validation from accepting an unqueryable snapshot.
func schemaPayloadFromLoadedCatalog(loaded loadedSchemaCatalog, args []string) (map[string]any, error) {
if len(args) == 0 {
snapshot, err := loaded.Registry.ToSnapshotPayload()
if err != nil {
return nil, err
}
payload := snapshot.Catalog
payload["catalog_hash"] = loaded.Snapshot.SourceHash
if loaded.Snapshot.SurfaceHash != "" {
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
}
return payload, nil
}
raw := strings.TrimSpace(args[0])
if tool, ok := loaded.Index.Resolve(raw); ok {
return schemaToolForResolvedPath(tool, raw).ToPayload()
}
tokens := splitSchemaPathTokens(raw)
if len(tokens) == 1 {
if product, ok := loaded.Index.Product(tokens[0]); ok {
payload, err := product.ToSummaryPayload()
if err != nil {
return nil, err
}
return map[string]any{
"kind": "schema",
"level": "product",
"count": len(product.Tools),
"product": payload,
"source": "embedded-command-catalog",
}, nil
}
}
if len(tokens) > 1 {
path := strings.Join(tokens, " ")
if product, ok := loaded.Index.Product(tokens[0]); ok {
matched := make([]map[string]any, 0)
for _, tool := range product.Tools {
if schemaToolUnderGroup(tool, path) {
summary, err := tool.ToSummaryPayload()
if err != nil {
return nil, err
}
matched = append(matched, summary)
}
}
if len(matched) > 0 {
return map[string]any{
"kind": "schema",
"level": "group",
"path": path,
"count": len(matched),
"tools": matched,
"source": "embedded-command-catalog",
}, nil
}
}
}
return nil, apperrors.NewValidation("unknown runtime schema path " + strconvQuote(raw))
}
func schemaCatalogSnapshotHash(snapshot SchemaCatalogSnapshot) string {
payload := struct {
Version int `json:"version"`
SurfaceHash string `json:"surface_hash,omitempty"`
Catalog map[string]any `json:"catalog"`
Tools map[string]map[string]any `json:"tools"`
}{snapshot.Version, snapshot.SurfaceHash, snapshot.Catalog, snapshot.Tools}
encoded, _ := json.Marshal(payload)
sum := sha256.Sum256(encoded)
return "sha256:" + hex.EncodeToString(sum[:])
}
func schemaMapSlice(value any) []map[string]any {
switch values := value.(type) {
case []map[string]any:
return values
case []any:
out := make([]map[string]any, 0, len(values))
for _, value := range values {
if item, ok := value.(map[string]any); ok {
out = append(out, item)
}
}
return out
default:
return nil
}
}
func schemaMap(value any) map[string]map[string]any {
input, ok := value.(map[string]any)
if !ok {
return nil
}
out := make(map[string]map[string]any, len(input))
for key, value := range input {
if item, ok := value.(map[string]any); ok {
out[key] = item
}
}
return out
}
func schemaString(value any) string {
valueString, _ := value.(string)
return valueString
}
func schemaStringSlice(value any) []string {
switch values := value.(type) {
case []string:
return values
case []any:
out := make([]string, 0, len(values))
for _, value := range values {
if item, ok := value.(string); ok {
out = append(out, item)
}
}
return out
default:
return nil
}
}
func firstNonEmptySchemaString(values ...any) string {
for _, value := range values {
if text := strings.TrimSpace(schemaString(value)); text != "" {
return text
}
}
return ""
}
File diff suppressed because one or more lines are too long
+348
View File
@@ -0,0 +1,348 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
package cli
import (
"sort"
"strings"
"testing"
)
func TestBuildSchemaCatalogSnapshotRejectsUnresolvedSource(t *testing.T) {
_, err := BuildSchemaCatalogSnapshot(ResolvedSchemaBuild{}, SchemaCatalogBuildOptions{})
if err == nil || !strings.Contains(err.Error(), "ResolveSchemaBuild") {
t.Fatalf("BuildSchemaCatalogSnapshot() error = %v, want resolved-source requirement", err)
}
}
func TestEmbeddedSchemaCatalogIntegrity(t *testing.T) {
loaded := embeddedSchemaCatalog()
if !embeddedSchemaCatalogAvailable() {
t.Fatal("embedded schema catalog is unavailable or failed integrity validation")
}
if got := schemaString(loaded.Snapshot.Catalog["source"]); got != "embedded-command-catalog" {
t.Fatalf("catalog source = %q", got)
}
}
func TestEmbeddedSchemaCatalogProgressiveQueries(t *testing.T) {
overview, err := embeddedSchemaOverviewPayload()
if err != nil {
t.Fatal(err)
}
if got, want := schemaProductToolCount(map[string]any{"tools": overview["products"]}), len(embeddedSchemaCatalog().Registry.Products); got != want {
t.Fatalf("compact product count = %d, want %d", got, want)
}
leaf, err := embeddedSchemaPayload([]string{"calendar event create"})
if err != nil {
t.Fatal(err)
}
if got := schemaString(leaf["canonical_path"]); got != "calendar.create_calendar_event" {
t.Fatalf("canonical path = %q", got)
}
if len(schemaMapSlice(leaf["parameters"])) != 0 {
t.Fatal("parameters unexpectedly decoded as a list")
}
if parameters, ok := leaf["parameters"].(map[string]any); !ok || len(parameters) == 0 {
t.Fatal("calendar.create_event parameters are empty")
}
group, err := embeddedSchemaPayload([]string{"calendar.event"})
if err != nil {
t.Fatal(err)
}
if schemaProductToolCount(map[string]any{"tools": group["tools"]}) == 0 {
t.Fatal("calendar.event group is empty")
}
alias, err := embeddedSchemaPayload([]string{"aitable record list"})
if err != nil {
t.Fatal(err)
}
if alias["is_alias"] != true || schemaString(alias["cli_path"]) != "aitable record list" {
t.Fatalf("alias query did not preserve compatibility path: %#v", alias)
}
if schemaString(alias["canonical_path"]) != "aitable.query_records" {
t.Fatalf("alias canonical path = %q", schemaString(alias["canonical_path"]))
}
}
func TestEmbeddedSchemaAllPayloadContainsEveryFullLeaf(t *testing.T) {
loaded := embeddedSchemaCatalog()
payload, err := embeddedSchemaAllPayload()
if err != nil {
t.Fatal(err)
}
expanded := 0
parameterized := 0
for _, product := range schemaMapSlice(payload["products"]) {
for _, tool := range schemaMapSlice(product["tools"]) {
canonical := schemaString(tool["canonical_path"])
expected, ok := loaded.Snapshot.Tools[canonical]
if !ok {
t.Fatalf("full export contains unknown tool %q", canonical)
}
parameters, ok := tool["parameters"].(map[string]any)
if !ok {
t.Fatalf("full export tool %s has no parameters object", canonical)
}
if len(parameters) > 0 {
parameterized++
}
if !schemaJSONEqual(tool, expected) {
t.Fatalf("full export tool %s differs from stored leaf Schema", canonical)
}
expanded++
}
}
if got, want := expanded, len(loaded.Snapshot.Tools); got != want {
t.Fatalf("full export tools = %d, want %d", got, want)
}
if parameterized == 0 {
t.Fatal("full export contains no parameterized tools")
}
}
func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *testing.T) {
leaf, err := embeddedSchemaPayload([]string{"chat category create-smart"})
if err != nil {
t.Fatal(err)
}
if got := schemaString(leaf["source"]); got != "reviewed_command_registry" {
t.Fatalf("source = %q, want reviewed_command_registry", got)
}
identity := schemaMap(leaf["field_provenance"])["canonical_path"]
if identity["source"] != "reviewed_command_registry" || identity["precedence"] != "command_registry" {
t.Fatalf("canonical identity provenance = %#v", identity)
}
parameters := schemaMap(leaf["parameters"])
assertReviewedManual := func(flagName, field string) {
t.Helper()
provenance := schemaMap(parameters[flagName]["field_provenance"])
winner := provenance[field]
if winner["source"] != "reviewed_manual_hint" || winner["precedence"] != "reviewed_manual" {
t.Fatalf("%s.%s provenance = %#v", flagName, field, winner)
}
}
name := parameters["name"]
if name["property"] != "categoryName" || name["required"] != true {
t.Fatalf("name parameter = %#v", name)
}
assertReviewedManual("name", "property")
assertReviewedManual("name", "required")
for flagName, property := range map[string]string{
"keywords": "groupNameKeywords",
"members": "memberOpenDingTalkIds",
} {
parameter := parameters[flagName]
if parameter["property"] != property || parameter["interface_type"] != "array" || parameter["required"] != false {
t.Fatalf("%s parameter = %#v", flagName, parameter)
}
for _, field := range []string{"property", "interface_type", "required"} {
assertReviewedManual(flagName, field)
}
}
}
func TestEmbeddedCatalogModelsAitableExportBranches(t *testing.T) {
leaf, err := embeddedSchemaPayload([]string{"aitable export data"})
if err != nil {
t.Fatal(err)
}
parameters := schemaMap(leaf["parameters"])
if _, exists := parameters["format"]; exists {
t.Fatal("business export format still shadows the global --format output flag")
}
exportFormat := parameters["export-format"]
if exportFormat["property"] != "format" || exportFormat["required"] != false {
t.Fatalf("export-format parameter = %#v", exportFormat)
}
if got, want := schemaStringSlice(exportFormat["enum"]), []string{"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}; !equalStringSlices(got, want) {
t.Fatalf("export-format enum = %v, want %v", got, want)
}
if parameters["scope"]["required"] != false {
t.Fatalf("scope must be conditional, got %#v", parameters["scope"])
}
if got := parameters["table-id"]["required_when"]; got != "scope is table or view" {
t.Fatalf("table-id required_when = %#v", got)
}
if got := parameters["view-id"]["required_when"]; got != "scope is view" {
t.Fatalf("view-id required_when = %#v", got)
}
constraints, ok := leaf["constraints"].(map[string]any)
if !ok {
t.Fatalf("constraints = %#v", leaf["constraints"])
}
hasGroup := func(field string, want ...string) bool {
groups, _ := constraints[field].([]any)
for _, raw := range groups {
if equalStringSlices(schemaStringSlice(raw), want) {
return true
}
}
return false
}
if !hasGroup("require_one_of", "scope", "task-id") ||
!hasGroup("require_one_of", "export-format", "task-id") ||
!hasGroup("require_together", "scope", "export-format") {
t.Fatalf("branch constraints = %#v", constraints)
}
}
func TestEmbeddedCatalogKeepsSharedFlagSemanticsCommandScoped(t *testing.T) {
queryLeaf, err := embeddedSchemaPayload([]string{"aitable record query"})
if err != nil {
t.Fatal(err)
}
getLeaf, err := embeddedSchemaPayload([]string{"aitable record get"})
if err != nil {
t.Fatal(err)
}
queryRecordIDs := schemaMap(queryLeaf["parameters"])["record-ids"]
getRecordIDs := schemaMap(getLeaf["parameters"])["record-ids"]
if queryRecordIDs["required"] != false {
t.Fatalf("record query --record-ids = %#v, want optional", queryRecordIDs)
}
if getRecordIDs["required"] != true {
t.Fatalf("record get --record-ids = %#v, want required", getRecordIDs)
}
getProvenance := schemaMap(getRecordIDs["field_provenance"])["required"]
if getProvenance["source"] != "typed_parameter_metadata" {
t.Fatalf("record get required provenance = %#v", getProvenance)
}
}
func equalStringSlices(left, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
func TestStripSchemaPayloadCompactLeaf(t *testing.T) {
leaf, err := embeddedSchemaPayload([]string{"calendar event create"})
if err != nil {
t.Fatal(err)
}
stripped := stripSchemaPayloadCompact(leaf)
// Must keep agent-essential fields.
for _, key := range []string{"cli_path", "canonical_path", "description", "effect", "risk", "confirmation", "parameters", "constraints"} {
if _, ok := stripped[key]; !ok {
t.Fatalf("compact leaf missing essential key %q", key)
}
}
// Must strip provenance / redundant fields.
for _, key := range []string{"agent_metadata_source", "agent_source_refs", "agent_summary_source", "effect_source", "metadata_source", "primary_cli_path", "parameter_count", "has_parameters", "interface_ref", "source", "title", "display"} {
if _, ok := stripped[key]; ok {
t.Fatalf("compact leaf still contains stripped key %q", key)
}
}
// Parameters must not contain interface_description / property.
if params, ok := stripped["parameters"].(map[string]any); ok {
for name, p := range params {
if pm, ok := p.(map[string]any); ok {
for _, stripped := range []string{"interface_description", "interface_type", "property"} {
if _, present := pm[stripped]; present {
t.Fatalf("compact param %q still contains %q", name, stripped)
}
}
// Must keep type and required.
if _, present := pm["type"]; !present {
t.Fatalf("compact param %q missing type", name)
}
}
}
}
}
func TestStripSchemaPayloadCompactPreservesParameterIdentity(t *testing.T) {
leaf, err := embeddedSchemaPayload([]string{"chat category create-smart"})
if err != nil {
t.Fatal(err)
}
full := schemaMap(leaf["parameters"])
compact := schemaMap(stripSchemaPayloadCompact(leaf)["parameters"])
if len(compact) != len(full) {
t.Fatalf("compact parameter count = %d, want %d: full=%v compact=%v", len(compact), len(full), sortedSchemaKeys(full), sortedSchemaKeys(compact))
}
name := compact["name"]
if name["required"] != true || name["type"] != "string" {
t.Fatalf("compact --name parameter = %#v", name)
}
synthetic := map[string]any{"parameters": map[string]any{}}
parameters := synthetic["parameters"].(map[string]any)
for _, parameterName := range []string{"name", "path", "source", "title", "group", "aliases"} {
parameters[parameterName] = map[string]any{"type": "string", "required": false, "field_provenance": map[string]any{"source": "test"}}
}
stripped := schemaMap(stripSchemaPayloadCompact(synthetic)["parameters"])
for parameterName := range parameters {
if _, ok := stripped[parameterName]; !ok {
t.Errorf("compact projection dropped parameter identity %q", parameterName)
}
}
}
func sortedSchemaKeys(values map[string]map[string]any) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func TestStripSchemaPayloadCompactOverview(t *testing.T) {
overview, err := embeddedSchemaOverviewPayload()
if err != nil {
t.Fatal(err)
}
stripped := stripSchemaPayloadCompact(overview)
// Overview must keep kind/level/count/products.
for _, key := range []string{"kind", "level", "count", "products"} {
if _, ok := stripped[key]; !ok {
t.Fatalf("compact overview missing key %q", key)
}
}
// Must strip agent_metadata / interface_metadata at top level.
for _, key := range []string{"agent_metadata", "interface_metadata", "source"} {
if _, ok := stripped[key]; ok {
t.Fatalf("compact overview still contains stripped key %q", key)
}
}
}
func TestStripSchemaPayloadCompactProduct(t *testing.T) {
product, err := embeddedSchemaPayload([]string{"calendar"})
if err != nil {
t.Fatal(err)
}
stripped := stripSchemaPayloadCompact(product)
if _, ok := stripped["product"]; !ok {
t.Fatal("compact product missing 'product' key")
}
prod := stripped["product"].(map[string]any)
for _, key := range []string{"agent_metadata_source", "agent_source_refs", "source"} {
if _, ok := prod[key]; ok {
t.Fatalf("compact product still contains stripped key %q", key)
}
}
}
+846
View File
@@ -0,0 +1,846 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli
import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// AliasKind records how a reviewed alias path is represented by Cobra. A
// Cobra alias resolves to the primary command pointer; a compatibility leaf is
// a separately registered (usually hidden) runnable command. Keeping the two
// forms explicit prevents downstream code from assuming that every alias has
// its own Cobra leaf.
type AliasKind string
const (
AliasKindCobraAlias AliasKind = "cobra_alias"
AliasKindCompatibilityLeaf AliasKind = "compatibility_leaf"
runtimeCompatibilityEquivalenceAnnotation = "dws.schema.compatibility_equivalence"
)
// RuntimeCompatibilityEquivalence is an implementation-side review record
// for two separately registered Cobra leaves that intentionally share one
// Schema identity despite using different execution handlers. Registry alias
// review alone is not enough: different handlers may inject fixed arguments or
// route to different business operations while exposing identical flags.
type RuntimeCompatibilityEquivalence struct {
ID string `json:"id"`
Reason string `json:"reason"`
Reviewed bool `json:"reviewed"`
}
// AnnotateRuntimeCompatibilityEquivalence records the same typed review on
// both sides of one independently executable compatibility pair. Invalid
// review data panics during command construction so production cannot silently
// accept an unreviewed handler mismatch.
func AnnotateRuntimeCompatibilityEquivalence(primary, compatibility *cobra.Command, review RuntimeCompatibilityEquivalence) {
if primary == nil || compatibility == nil {
panic("runtime compatibility equivalence requires two commands")
}
review.ID = strings.TrimSpace(review.ID)
review.Reason = strings.TrimSpace(review.Reason)
if review.ID == "" || review.Reason == "" || !review.Reviewed {
panic("runtime compatibility equivalence requires a reviewed id and reason")
}
encoded, err := json.Marshal(review)
if err != nil {
panic(fmt.Sprintf("encode runtime compatibility equivalence: %v", err))
}
commands := []*cobra.Command{primary, compatibility}
for _, command := range commands {
if existing, exists := command.Annotations[runtimeCompatibilityEquivalenceAnnotation]; exists && existing != string(encoded) {
panic(fmt.Sprintf("runtime compatibility equivalence for %q conflicts with an existing reviewed marker", command.CommandPath()))
}
}
for _, command := range commands {
if command.Annotations == nil {
command.Annotations = map[string]string{}
}
command.Annotations[runtimeCompatibilityEquivalenceAnnotation] = string(encoded)
}
}
// BoundAlias is one reviewed alias path resolved against the live Cobra tree.
type BoundAlias struct {
Path string
Command *cobra.Command
Kind AliasKind
Source string
ReviewReason string
}
// exactCobraPathMatch is the result of resolving one reviewed CLI path. The
// resolver records whether any path segment used Cobra's Aliases metadata so
// callers can distinguish a same-command Cobra alias from a separately
// registered compatibility leaf.
type exactCobraPathMatch struct {
Command *cobra.Command
UsedAlias bool
}
// BoundCommandSpec joins one stable registry identity to its executable
// primary leaf and all executable alias paths.
type BoundCommandSpec struct {
CommandSpec
PrimaryCommand *cobra.Command
AliasCommands []BoundAlias
}
// BoundCommandRegistry is the only command input the Schema assembler should
// consume. Every entry has passed exact path, executable-leaf, collision, and
// native-annotation consistency checks.
type BoundCommandRegistry struct {
Commands []BoundCommandSpec
ByCanonical map[string]BoundCommandSpec
ByCLIPath map[string]BoundCommandSpec
}
// BindEffectiveCommandRegistry resolves every registry path against the live
// Cobra tree. Registry entries may target hidden compatibility leaves when
// they are explicitly reviewed, but every target must be an actual runnable
// leaf. Native annotations are optional implementation evidence; when present
// they must exactly agree with the registry identity.
func BindEffectiveCommandRegistry(root *cobra.Command, effective EffectiveCommandRegistry) (BoundCommandRegistry, error) {
if root == nil {
return BoundCommandRegistry{}, fmt.Errorf("bind effective Schema command registry: root is nil")
}
if err := ValidateEmbeddedSchemaParameterBindings(); err != nil {
return BoundCommandRegistry{}, fmt.Errorf("validate reviewed Schema parameter bindings: %w", err)
}
validated, err := newEffectiveCommandRegistry(effective.Commands)
if err != nil {
return BoundCommandRegistry{}, fmt.Errorf("bind effective Schema command registry: %w", err)
}
bound := BoundCommandRegistry{
Commands: make([]BoundCommandSpec, 0, len(validated.Commands)),
ByCanonical: make(map[string]BoundCommandSpec, len(validated.Commands)),
ByCLIPath: make(map[string]BoundCommandSpec, len(validated.ByCLIPath)),
}
for _, spec := range validated.Commands {
primary, err := bindCommandRegistryPath(root, spec, spec.PrimaryCLIPath)
if err != nil {
return BoundCommandRegistry{}, err
}
item := BoundCommandSpec{
CommandSpec: cloneCommandSpec(spec),
PrimaryCommand: primary,
AliasCommands: make([]BoundAlias, 0, len(spec.Aliases)),
}
for _, alias := range spec.Aliases {
boundAlias, err := bindCommandRegistryAlias(root, spec, primary, alias)
if err != nil {
return BoundCommandRegistry{}, err
}
item.AliasCommands = append(item.AliasCommands, boundAlias)
}
bound.Commands = append(bound.Commands, item)
}
sort.Slice(bound.Commands, func(i, j int) bool {
return bound.Commands[i].CanonicalPath < bound.Commands[j].CanonicalPath
})
for _, item := range bound.Commands {
bound.ByCanonical[item.CanonicalPath] = item
bound.ByCLIPath[item.PrimaryCLIPath] = item
for _, alias := range item.Aliases {
bound.ByCLIPath[alias] = item
}
}
return bound, nil
}
func bindCommandRegistryAlias(root *cobra.Command, spec CommandSpec, primary *cobra.Command, rawPath string) (BoundAlias, error) {
path := normalizeSchemaCLIPath(rawPath)
match, err := resolveExactCobraPath(root, path)
if err != nil {
return BoundAlias{}, fmt.Errorf("schema command registry %s alias %q: %w", spec.CanonicalPath, path, err)
}
if match.Command == nil {
return BoundAlias{}, fmt.Errorf("schema command registry %s has stale alias path %q: command does not exist", spec.CanonicalPath, path)
}
if !runnableSchemaLeaf(match.Command) {
return BoundAlias{}, fmt.Errorf("schema command registry %s alias %q is not a runnable Cobra leaf", spec.CanonicalPath, path)
}
// A Cobra alias is the same runnable command reached through one or more
// Aliases segments. A compatibility leaf is a separately registered exact
// Name path. A Cobra alias that reaches another command pointer is neither
// representation and fails closed.
kind := AliasKindCompatibilityLeaf
if match.UsedAlias {
if match.Command != primary {
return BoundAlias{}, fmt.Errorf("schema command registry %s Cobra alias %q resolves to a different command than primary path %q", spec.CanonicalPath, path, spec.PrimaryCLIPath)
}
kind = AliasKindCobraAlias
} else if match.Command == primary {
return BoundAlias{}, fmt.Errorf("schema command registry %s alias %q duplicates primary path %q", spec.CanonicalPath, path, spec.PrimaryCLIPath)
}
if err := validateCommandRegistryAnnotation(match.Command, path, spec); err != nil {
return BoundAlias{}, err
}
if kind == AliasKindCompatibilityLeaf {
if err := validateCompatibilityLeafContract(spec, primary, match.Command, path); err != nil {
return BoundAlias{}, err
}
}
return BoundAlias{
Path: path,
Command: match.Command,
Kind: kind,
Source: spec.Source,
ReviewReason: spec.ReviewReason,
}, nil
}
// compatibilityFlagContract is the executable and Schema-relevant portion of
// one effective Cobra flag. A separately registered compatibility leaf is a
// reviewed alias only when it accepts the same effective flag surface as its
// primary command. Presentation on the command itself may differ (for example,
// a deprecation notice), but flag parsing and parameter facts may not.
type compatibilityFlagContract struct {
Type string
Default string
NoOptDefault string
Shorthand string
Hidden bool
Deprecated string
ShorthandDeprecated string
Origin string
Annotations map[string][]string
Required compatibilityFlagRequiredContract
}
type compatibilityFlagRequiredContract struct {
CobraRequired bool
UsageRequired bool
UsageDefault bool
NativeRequired string
MetadataRequired string
NativeRequiredWhen string
MetadataRequiredWhen string
}
// validateCompatibilityLeafContract proves that an independently executable
// compatibility leaf has the same invocation contract as the reviewed primary
// leaf. Unlike a Cobra alias, it is a different command pointer and can drift
// even while both paths still resolve to the same canonical identity.
func validateCompatibilityLeafContract(spec CommandSpec, primary, alias *cobra.Command, aliasPath string) error {
problems := make([]string, 0)
problems = append(problems, compatibilityHandlerContractProblems(primary, alias)...)
flagProblems, err := compatibilityFlagContractProblems(spec.CanonicalPath, primary, alias)
if err != nil {
return fmt.Errorf("validate reviewed Schema parameter bindings for compatibility leaf %q: %w", aliasPath, err)
}
problems = append(problems, flagProblems...)
problems = append(problems, compatibilityArgsContractProblems(primary, alias)...)
primaryConstraints, err := strictCompatibilityConstraints(primary, spec.CanonicalPath)
if err != nil {
problems = append(problems, "primary runtime constraints are invalid: "+err.Error())
}
aliasConstraints, aliasErr := strictCompatibilityConstraints(alias, spec.CanonicalPath)
if aliasErr != nil {
problems = append(problems, "compatibility runtime constraints are invalid: "+aliasErr.Error())
}
if err == nil && aliasErr == nil && !reflect.DeepEqual(primaryConstraints, aliasConstraints) {
problems = append(problems, fmt.Sprintf("runtime constraints differ: primary=%s compatibility=%s",
compatibilityJSON(primaryConstraints), compatibilityJSON(aliasConstraints)))
}
primaryPositionals, err := strictCompatibilityPositionals(primary)
if err != nil {
problems = append(problems, "primary runtime positionals are invalid: "+err.Error())
}
aliasPositionals, aliasErr := strictCompatibilityPositionals(alias)
if aliasErr != nil {
problems = append(problems, "compatibility runtime positionals are invalid: "+aliasErr.Error())
}
if err == nil && aliasErr == nil && !reflect.DeepEqual(primaryPositionals, aliasPositionals) {
problems = append(problems, fmt.Sprintf("runtime positionals differ: primary=%s compatibility=%s",
compatibilityJSON(primaryPositionals), compatibilityJSON(aliasPositionals)))
}
if len(problems) == 0 {
return nil
}
return fmt.Errorf(
"schema command registry %s compatibility leaf alias %q is not executable-contract equivalent to primary path %q:\n - %s",
spec.CanonicalPath,
aliasPath,
spec.PrimaryCLIPath,
strings.Join(problems, "\n - "),
)
}
func compatibilityHandlerContractProblems(primary, alias *cobra.Command) []string {
if primary == nil || alias == nil {
return []string{"execution handlers cannot be compared for a nil command"}
}
differences := make([]string, 0)
for _, handler := range []struct {
name string
primary any
alias any
}{
{name: "PersistentPreRun", primary: primary.PersistentPreRun, alias: alias.PersistentPreRun},
{name: "PersistentPreRunE", primary: primary.PersistentPreRunE, alias: alias.PersistentPreRunE},
{name: "PreRun", primary: primary.PreRun, alias: alias.PreRun},
{name: "PreRunE", primary: primary.PreRunE, alias: alias.PreRunE},
{name: "Run", primary: primary.Run, alias: alias.Run},
{name: "RunE", primary: primary.RunE, alias: alias.RunE},
{name: "PostRun", primary: primary.PostRun, alias: alias.PostRun},
{name: "PostRunE", primary: primary.PostRunE, alias: alias.PostRunE},
{name: "PersistentPostRun", primary: primary.PersistentPostRun, alias: alias.PersistentPostRun},
{name: "PersistentPostRunE", primary: primary.PersistentPostRunE, alias: alias.PersistentPostRunE},
} {
if compatibilityHandlerPointer(handler.primary) != compatibilityHandlerPointer(handler.alias) {
differences = append(differences, handler.name)
}
}
primaryReview, primaryPresent, primaryErr := runtimeCompatibilityEquivalence(primary)
aliasReview, aliasPresent, aliasErr := runtimeCompatibilityEquivalence(alias)
problems := make([]string, 0)
if primaryErr != nil {
problems = append(problems, "primary compatibility equivalence is invalid: "+primaryErr.Error())
}
if aliasErr != nil {
problems = append(problems, "compatibility equivalence is invalid: "+aliasErr.Error())
}
reviewMatches := primaryPresent && aliasPresent && primaryErr == nil && aliasErr == nil && reflect.DeepEqual(primaryReview, aliasReview)
if !primaryPresent && !aliasPresent {
problems = append(problems, "independent compatibility leaves require the same reviewed typed compatibility equivalence on both commands")
} else if primaryPresent != aliasPresent {
problems = append(problems, "reviewed compatibility equivalence must be present on both commands")
} else if primaryPresent && aliasPresent && primaryErr == nil && aliasErr == nil && !reflect.DeepEqual(primaryReview, aliasReview) {
problems = append(problems, fmt.Sprintf("reviewed compatibility equivalence differs: primary=%s compatibility=%s",
compatibilityJSON(primaryReview), compatibilityJSON(aliasReview)))
}
if len(differences) > 0 && !reviewMatches {
problems = append(problems, fmt.Sprintf("execution handler implementation differs for %s; add the same reviewed typed compatibility equivalence to both commands or model them as distinct canonical tools", strings.Join(differences, ", ")))
}
return problems
}
func compatibilityHandlerPointer(handler any) uintptr {
if handler == nil {
return 0
}
value := reflect.ValueOf(handler)
if value.Kind() != reflect.Func || value.IsNil() {
return 0
}
return value.Pointer()
}
func runtimeCompatibilityEquivalence(command *cobra.Command) (RuntimeCompatibilityEquivalence, bool, error) {
if command == nil || command.Annotations == nil {
return RuntimeCompatibilityEquivalence{}, false, nil
}
raw, present := command.Annotations[runtimeCompatibilityEquivalenceAnnotation]
if !present {
return RuntimeCompatibilityEquivalence{}, false, nil
}
raw = strings.TrimSpace(raw)
if raw == "" {
return RuntimeCompatibilityEquivalence{}, true, fmt.Errorf("annotation is empty")
}
var review RuntimeCompatibilityEquivalence
decoder := json.NewDecoder(bytes.NewBufferString(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&review); err != nil {
return RuntimeCompatibilityEquivalence{}, true, err
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
err = fmt.Errorf("multiple JSON values")
}
return RuntimeCompatibilityEquivalence{}, true, err
}
if review.ID == "" || review.ID != strings.TrimSpace(review.ID) || review.Reason == "" || review.Reason != strings.TrimSpace(review.Reason) || !review.Reviewed {
return RuntimeCompatibilityEquivalence{}, true, fmt.Errorf("annotation requires an exact reviewed id and reason")
}
return review, true, nil
}
func compatibilityFlagContractProblems(canonicalPath string, primary, alias *cobra.Command) ([]string, error) {
primaryFlags, err := effectiveCompatibilityFlagContracts(primary, canonicalPath)
if err != nil {
return nil, err
}
aliasFlags, err := effectiveCompatibilityFlagContracts(alias, canonicalPath)
if err != nil {
return nil, err
}
names := make([]string, 0, len(primaryFlags)+len(aliasFlags))
seen := map[string]bool{}
for name := range primaryFlags {
seen[name] = true
names = append(names, name)
}
for name := range aliasFlags {
if !seen[name] {
names = append(names, name)
}
}
sort.Strings(names)
problems := make([]string, 0)
for _, name := range names {
primaryFlag, primaryOK := primaryFlags[name]
aliasFlag, aliasOK := aliasFlags[name]
switch {
case !primaryOK:
problems = append(problems, fmt.Sprintf("flag --%s exists only on compatibility leaf", name))
continue
case !aliasOK:
problems = append(problems, fmt.Sprintf("flag --%s is missing from compatibility leaf", name))
continue
}
if primaryFlag.Type != aliasFlag.Type {
problems = append(problems, fmt.Sprintf("flag --%s type differs: primary=%q compatibility=%q", name, primaryFlag.Type, aliasFlag.Type))
}
if primaryFlag.Default != aliasFlag.Default {
problems = append(problems, fmt.Sprintf("flag --%s default differs: primary=%q compatibility=%q", name, primaryFlag.Default, aliasFlag.Default))
}
if primaryFlag.NoOptDefault != aliasFlag.NoOptDefault {
problems = append(problems, fmt.Sprintf("flag --%s no-option default differs: primary=%q compatibility=%q", name, primaryFlag.NoOptDefault, aliasFlag.NoOptDefault))
}
if primaryFlag.Shorthand != aliasFlag.Shorthand {
problems = append(problems, fmt.Sprintf("flag --%s shorthand differs: primary=%q compatibility=%q", name, primaryFlag.Shorthand, aliasFlag.Shorthand))
}
if primaryFlag.Hidden != aliasFlag.Hidden {
problems = append(problems, fmt.Sprintf("flag --%s hidden state differs: primary=%t compatibility=%t", name, primaryFlag.Hidden, aliasFlag.Hidden))
}
if primaryFlag.Deprecated != aliasFlag.Deprecated {
problems = append(problems, fmt.Sprintf("flag --%s deprecation behavior differs: primary=%q compatibility=%q", name, primaryFlag.Deprecated, aliasFlag.Deprecated))
}
if primaryFlag.ShorthandDeprecated != aliasFlag.ShorthandDeprecated {
problems = append(problems, fmt.Sprintf("flag --%s shorthand deprecation differs: primary=%q compatibility=%q", name, primaryFlag.ShorthandDeprecated, aliasFlag.ShorthandDeprecated))
}
if primaryFlag.Origin != aliasFlag.Origin {
problems = append(problems, fmt.Sprintf("flag --%s local/persistent/inherited behavior differs: primary=%s compatibility=%s", name, primaryFlag.Origin, aliasFlag.Origin))
}
if !reflect.DeepEqual(primaryFlag.Required, aliasFlag.Required) {
problems = append(problems, fmt.Sprintf("flag --%s required/required_when facts differ: primary=%s compatibility=%s",
name, compatibilityJSON(primaryFlag.Required), compatibilityJSON(aliasFlag.Required)))
}
if !reflect.DeepEqual(primaryFlag.Annotations, aliasFlag.Annotations) {
problems = append(problems, fmt.Sprintf("flag --%s annotations differ: primary=%s compatibility=%s",
name, compatibilityJSON(primaryFlag.Annotations), compatibilityJSON(aliasFlag.Annotations)))
}
}
return problems, nil
}
func effectiveCompatibilityFlagContracts(command *cobra.Command, canonicalPath string) (map[string]compatibilityFlagContract, error) {
contracts := map[string]compatibilityFlagContract{}
if command == nil {
return contracts, nil
}
canonicalPath = strings.TrimSpace(canonicalPath)
snapshot, err := runtimeSchemaParameterBindingData()
if err != nil {
return nil, err
}
bindings := snapshot.Bindings[canonicalPath]
metadata := runtimeSchemaParameterMetadataByCanonical[canonicalPath]
// Ask Cobra to materialize inherited flags, then visit every contributing
// set explicitly. In particular, a newly-added persistent flag on a leaf is
// not guaranteed to have been merged into Flags() yet.
command.InheritedFlags()
visit := func(flag *pflag.Flag) {
// Cobra materializes --help lazily on only the command being executed.
// It is framework scaffolding, not part of a leaf's executable/Schema
// parameter contract, and must not make compatibility equivalence depend
// on which alias happened to run before binding.
if flag == nil || flag.Name == "help" {
return
}
flagType := ""
if flag.Value != nil {
flagType = flag.Value.Type()
}
annotations := effectiveCompatibilityFlagAnnotations(flag, bindings, metadata)
contracts[flag.Name] = compatibilityFlagContract{
Type: flagType,
Default: flag.DefValue,
NoOptDefault: flag.NoOptDefVal,
Shorthand: flag.Shorthand,
Hidden: flag.Hidden,
Deprecated: flag.Deprecated,
ShorthandDeprecated: flag.ShorthandDeprecated,
Origin: compatibilityFlagOrigin(command, flag.Name),
Annotations: annotations,
Required: compatibilityFlagRequiredContract{
CobraRequired: runtimeFlagCobraHardRequired(flag),
UsageRequired: usageImpliesRequired(flag.Usage),
UsageDefault: usageImpliesDefault(flag.Usage),
NativeRequired: firstCompatibilityAnnotation(annotations, runtimeSchemaFlagRequiredAnnotation),
MetadataRequired: firstCompatibilityAnnotation(annotations, runtimeSchemaFlagMetadataRequiredAnnotation),
NativeRequiredWhen: firstCompatibilityAnnotation(annotations, runtimeSchemaFlagRequiredWhenAnnotation),
MetadataRequiredWhen: firstCompatibilityAnnotation(annotations, runtimeSchemaFlagMetadataRequiredWhenAnnotation),
},
}
}
command.Flags().VisitAll(visit)
command.PersistentFlags().VisitAll(visit)
command.InheritedFlags().VisitAll(visit)
return contracts, nil
}
// effectiveCompatibilityFlagAnnotations overlays canonical, code-owned
// parameter facts without mutating either command. Schema collection applies
// the same facts to the primary leaf later; modelling them here keeps repeated
// binding order-independent while still exposing path-specific drift.
func effectiveCompatibilityFlagAnnotations(flag *pflag.Flag, bindings map[string]string, metadata RuntimeSchemaParameterMetadata) map[string][]string {
annotations := cloneCompatibilityFlagAnnotations(flag.Annotations)
// Reviewed Manual Schema hints are canonical ToolSpec projection inputs,
// not executable facts owned by each Cobra path. They are intentionally
// attached to and resolved from the primary command once; a compatibility
// leaf remains a navigation view of that same ToolSpec. Keep every other
// native/typed annotation in the equivalence check so real command drift
// still fails closed.
delete(annotations, runtimeSchemaManualParameterAnnotation)
delete(annotations, runtimeSchemaManualReasonAnnotation)
if len(annotations) == 0 {
annotations = nil
}
set := func(key, value string) {
if value = strings.TrimSpace(value); value != "" {
if annotations == nil {
annotations = map[string][]string{}
}
annotations[key] = []string{value}
}
}
set(runtimeSchemaFlagBindingPropertyAnnotation, bindings[flag.Name])
for _, name := range metadata.Required {
if strings.TrimSpace(name) == flag.Name {
set(runtimeSchemaFlagMetadataRequiredAnnotation, "true")
break
}
}
set(runtimeSchemaFlagMetadataRequiredWhenAnnotation, metadata.RequiredWhen[flag.Name])
set(runtimeSchemaFlagMetadataFormatAnnotation, metadata.Formats[flag.Name])
set(runtimeSchemaFlagMetadataExampleAnnotation, metadata.Examples[flag.Name])
if values := metadata.Enums[flag.Name]; len(values) > 0 {
if annotations == nil {
annotations = map[string][]string{}
}
annotations[runtimeSchemaFlagMetadataEnumAnnotation] = append([]string(nil), values...)
}
return annotations
}
func firstCompatibilityAnnotation(annotations map[string][]string, key string) string {
for _, value := range annotations[key] {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}
func compatibilityFlagOrigin(command *cobra.Command, name string) string {
if command.PersistentFlags().Lookup(name) != nil {
return "local_persistent"
}
if command.LocalNonPersistentFlags().Lookup(name) != nil {
return "local"
}
if command.InheritedFlags().Lookup(name) != nil {
return "inherited_persistent"
}
return "effective_unknown"
}
func cloneCompatibilityFlagAnnotations(source map[string][]string) map[string][]string {
if len(source) == 0 {
return nil
}
clone := make(map[string][]string, len(source))
for key, values := range source {
clone[key] = append([]string(nil), values...)
}
return clone
}
func compatibilityArgsContractProblems(primary, alias *cobra.Command) []string {
problems := make([]string, 0)
if primary == nil || alias == nil {
return append(problems, "Args contract cannot be compared for a nil command")
}
if primary.DisableFlagParsing != alias.DisableFlagParsing {
problems = append(problems, fmt.Sprintf("DisableFlagParsing differs: primary=%t compatibility=%t", primary.DisableFlagParsing, alias.DisableFlagParsing))
}
if primary.TraverseChildren != alias.TraverseChildren {
problems = append(problems, fmt.Sprintf("TraverseChildren differs: primary=%t compatibility=%t", primary.TraverseChildren, alias.TraverseChildren))
}
if !reflect.DeepEqual(primary.FParseErrWhitelist, alias.FParseErrWhitelist) {
problems = append(problems, fmt.Sprintf("flag parse error allowlist differs: primary=%+v compatibility=%+v", primary.FParseErrWhitelist, alias.FParseErrWhitelist))
}
primarySyntax := compatibilityUseArgsSyntax(primary)
aliasSyntax := compatibilityUseArgsSyntax(alias)
if primarySyntax != aliasSyntax {
problems = append(problems, fmt.Sprintf("positional Use contract differs: primary=%q compatibility=%q", primarySyntax, aliasSyntax))
}
if !reflect.DeepEqual(primary.ValidArgs, alias.ValidArgs) {
problems = append(problems, fmt.Sprintf("ValidArgs differ: primary=%q compatibility=%q", primary.ValidArgs, alias.ValidArgs))
}
if !reflect.DeepEqual(primary.ArgAliases, alias.ArgAliases) {
problems = append(problems, fmt.Sprintf("ArgAliases differ: primary=%q compatibility=%q", primary.ArgAliases, alias.ArgAliases))
}
if (primary.Args == nil) != (alias.Args == nil) {
problems = append(problems, fmt.Sprintf("Args validator presence differs: primary=%t compatibility=%t", primary.Args != nil, alias.Args != nil))
return problems
}
if primary.Args == nil {
return problems
}
if reflect.ValueOf(primary.Args).Pointer() != reflect.ValueOf(alias.Args).Pointer() {
problems = append(problems, "Args validator implementation differs; reuse the primary validator for a reviewed compatibility leaf")
return problems
}
for _, probe := range compatibilityArgsProbes(primary, alias) {
primaryResult := runCompatibilityArgsValidator(primary, probe)
aliasResult := runCompatibilityArgsValidator(alias, probe)
if primaryResult != aliasResult {
problems = append(problems, fmt.Sprintf("Args behavior differs for %d positional argument(s): primary=%s compatibility=%s", len(probe), primaryResult, aliasResult))
break
}
}
return problems
}
func compatibilityUseArgsSyntax(command *cobra.Command) string {
parts := strings.Fields(command.Use)
if len(parts) <= 1 {
return ""
}
return strings.Join(parts[1:], " ")
}
func compatibilityArgsProbes(primary, alias *cobra.Command) [][]string {
// Count validators are by far the most common Cobra Args contract. Probe a
// deliberately broad range so separately-created Exact/Range/Maximum
// closures with the same code pointer cannot hide different captured bounds.
probes := make([][]string, 0, 260)
for count := 0; count <= 256; count++ {
args := make([]string, count)
for index := range args {
args[index] = fmt.Sprintf("arg-%d", index)
}
probes = append(probes, args)
}
for _, command := range []*cobra.Command{primary, alias} {
for _, value := range command.ValidArgs {
probes = append(probes, []string{string(value)})
}
for _, value := range command.ArgAliases {
probes = append(probes, []string{value})
}
}
probes = append(probes, []string{""}, []string{"unknown-value"})
return probes
}
func runCompatibilityArgsValidator(command *cobra.Command, args []string) (result string) {
result = "accepted"
defer func() {
if recovered := recover(); recovered != nil {
result = fmt.Sprintf("panic(%v)", recovered)
}
}()
if err := command.Args(command, append([]string(nil), args...)); err != nil {
return "rejected"
}
return result
}
func strictCompatibilityConstraints(command *cobra.Command, canonicalPath string) (RuntimeSchemaConstraints, error) {
var constraints RuntimeSchemaConstraints
if command != nil && command.Annotations != nil {
raw := strings.TrimSpace(command.Annotations[runtimeSchemaRulesAnnotation])
if raw != "" {
if err := decodeStrictSchemaJSON([]byte(raw), &constraints); err != nil {
return RuntimeSchemaConstraints{}, err
}
}
}
// RegisterRuntimeSchemaConstraints is canonical code-owned input. Collection
// attaches it to the primary leaf, so compare the same effective union on
// both executable paths without changing the live Cobra tree here.
registered := runtimeSchemaConstraintsByCanonical[strings.TrimSpace(canonicalPath)]
constraints.MutuallyExclusive = append(constraints.MutuallyExclusive, registered.MutuallyExclusive...)
constraints.RequireOneOf = append(constraints.RequireOneOf, registered.RequireOneOf...)
constraints.RequireTogether = append(constraints.RequireTogether, registered.RequireTogether...)
constraints = normalizeRuntimeSchemaConstraints(constraints)
constraints.MutuallyExclusive = canonicalCompatibilityGroups(constraints.MutuallyExclusive)
constraints.RequireOneOf = canonicalCompatibilityGroups(constraints.RequireOneOf)
constraints.RequireTogether = canonicalCompatibilityGroups(constraints.RequireTogether)
return constraints, nil
}
func canonicalCompatibilityGroups(groups [][]string) [][]string {
canonical := make([][]string, 0, len(groups))
for _, group := range groups {
group = append([]string(nil), group...)
sort.Strings(group)
canonical = append(canonical, group)
}
sort.Slice(canonical, func(i, j int) bool {
return strings.Join(canonical[i], "\x00") < strings.Join(canonical[j], "\x00")
})
return canonical
}
func strictCompatibilityPositionals(command *cobra.Command) ([]RuntimeSchemaPositional, error) {
if command == nil || command.Annotations == nil {
return nil, nil
}
raw := strings.TrimSpace(command.Annotations[runtimeSchemaArgsAnnotation])
if raw == "" {
return nil, nil
}
var positionals []RuntimeSchemaPositional
if err := decodeStrictSchemaJSON([]byte(raw), &positionals); err != nil {
return nil, err
}
sort.Slice(positionals, func(i, j int) bool {
if positionals[i].Index != positionals[j].Index {
return positionals[i].Index < positionals[j].Index
}
return positionals[i].Name < positionals[j].Name
})
return positionals, nil
}
func compatibilityJSON(value any) string {
encoded, err := json.Marshal(value)
if err != nil {
return fmt.Sprintf("%+v", value)
}
return string(encoded)
}
// resolveExactCobraPath resolves only exact command names and exact Cobra
// aliases. At every level a real command Name takes precedence over Aliases;
// multiple matching Names or Aliases fail closed. It intentionally does not
// use cobra.Command.Find because Find may apply prefix matching and
// suggestions, neither of which is a stable registry binding contract.
func resolveExactCobraPath(root *cobra.Command, rawPath string) (exactCobraPathMatch, error) {
if root == nil {
return exactCobraPathMatch{}, nil
}
parts := strings.Fields(strings.TrimSpace(rawPath))
if len(parts) > 0 && parts[0] == root.Name() {
parts = parts[1:]
}
if len(parts) == 0 {
return exactCobraPathMatch{}, nil
}
current := root
usedAlias := false
for _, part := range parts {
var exactMatches []*cobra.Command
for _, child := range current.Commands() {
if child.Name() == part {
exactMatches = appendDistinctCobraCommand(exactMatches, child)
}
}
if len(exactMatches) > 1 {
return exactCobraPathMatch{}, fmt.Errorf("cobra command segment %q is ambiguous", part)
}
if len(exactMatches) == 1 {
current = exactMatches[0]
continue
}
var aliasMatches []*cobra.Command
for _, child := range current.Commands() {
for _, alias := range child.Aliases {
if alias != part {
continue
}
aliasMatches = appendDistinctCobraCommand(aliasMatches, child)
}
}
if len(aliasMatches) > 1 {
return exactCobraPathMatch{}, fmt.Errorf("cobra alias segment %q is ambiguous", part)
}
if len(aliasMatches) == 0 {
return exactCobraPathMatch{}, nil
}
usedAlias = true
current = aliasMatches[0]
}
return exactCobraPathMatch{Command: current, UsedAlias: usedAlias}, nil
}
func appendDistinctCobraCommand(commands []*cobra.Command, command *cobra.Command) []*cobra.Command {
for _, existing := range commands {
if existing == command {
return commands
}
}
return append(commands, command)
}
func bindCommandRegistryPath(root *cobra.Command, spec CommandSpec, path string) (*cobra.Command, error) {
path = normalizeSchemaCLIPath(path)
match, err := resolveExactCobraPath(root, path)
if err != nil {
return nil, fmt.Errorf("schema command registry %s primary path %q: %w", spec.CanonicalPath, path, err)
}
if match.Command == nil {
return nil, fmt.Errorf("schema command registry %s has stale cli path %q: command does not exist", spec.CanonicalPath, path)
}
if match.UsedAlias {
return nil, fmt.Errorf("schema command registry %s primary path %q must use real Cobra command names, not Aliases", spec.CanonicalPath, path)
}
if !runnableSchemaLeaf(match.Command) {
return nil, fmt.Errorf("schema command registry %s path %q is not a runnable Cobra leaf", spec.CanonicalPath, path)
}
if err := validateCommandRegistryAnnotation(match.Command, path, spec); err != nil {
return nil, err
}
return match.Command, nil
}
func runnableSchemaLeaf(command *cobra.Command) bool {
return command != nil && command.Runnable() && !command.HasSubCommands()
}
func validateCommandRegistryAnnotation(command *cobra.Command, path string, spec CommandSpec) error {
nativeProduct, nativeTool, _ := runtimeSchemaAnnotations(command)
if nativeProduct != "" || nativeTool != "" {
if nativeProduct == "" || nativeTool == "" {
return fmt.Errorf("schema command registry %s path %q has incomplete native annotation %q.%q", spec.CanonicalPath, path, nativeProduct, nativeTool)
}
nativeCanonical := nativeProduct + "." + nativeTool
if nativeCanonical != spec.CanonicalPath {
return fmt.Errorf("schema command registry %s path %q conflicts with native annotation %s", spec.CanonicalPath, path, nativeCanonical)
}
}
if manualProduct, manualTool, _, ok := runtimeManualSchemaIdentity(command); ok {
manualCanonical := strings.TrimSpace(manualProduct + "." + manualTool)
if manualCanonical != spec.CanonicalPath {
return fmt.Errorf("schema command registry %s path %q conflicts with reviewed manual identity %s", spec.CanonicalPath, path, manualCanonical)
}
}
return nil
}
+933
View File
@@ -0,0 +1,933 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli
import (
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestBuildEffectiveCommandRegistryMergesReviewedManualCommands(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy", "helper add")
annotateTestCompatibilityPair(exactSchemaCommand(root, "item get"), exactSchemaCommand(root, "item legacy"))
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{
{
CLIPath: "item get",
CanonicalPath: "item.get_item",
Reason: "Confirms the reviewed primary identity",
Reviewed: true,
},
{
CLIPath: "item legacy",
CanonicalPath: "item.get_item",
Reason: "Reviewed compatibility alias",
Reviewed: true,
},
{
CLIPath: "helper add",
CanonicalPath: "helper.add_helper",
Reason: "Reviewed existing helper command",
Reviewed: true,
},
},
}
effective, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err != nil {
t.Fatalf("buildEffectiveCommandRegistry() error = %v", err)
}
if got := len(effective.Commands); got != 2 {
t.Fatalf("command count = %d, want 2", got)
}
item := effective.ByCanonical["item.get_item"]
if strings.Join(item.Aliases, ",") != "item legacy" {
t.Fatalf("item aliases = %#v", item.Aliases)
}
helper := effective.ByCanonical["helper.add_helper"]
if helper.Source != "reviewed_manual_hint" || helper.ReviewReason != "Reviewed existing helper command" || helper.Visibility != SchemaVisibilityPublic {
t.Fatalf("manual helper source = %#v", helper)
}
if effective.SourceHash() == reviewed.SourceHash() {
t.Fatal("manual-only command was omitted from the effective registry hash")
}
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry(effective) error = %v", err)
}
if got := bound.ByCLIPath["helper add"].CanonicalPath; got != "helper.add_helper" {
t.Fatalf("manual-only bound canonical = %q", got)
}
registry, err := AssembleSchemaRegistryFromBound(bound)
if err != nil {
t.Fatalf("AssembleSchemaRegistryFromBound(effective) error = %v", err)
}
index, err := registry.Index()
if err != nil {
t.Fatalf("manual-only registry index error = %v", err)
}
if _, ok := index.Resolve("helper.add_helper"); !ok {
t.Fatal("manual-only command was dropped before final SchemaRegistry delivery")
}
snapshot, err := registry.ToSnapshotPayload()
if err != nil {
t.Fatalf("manual-only snapshot serialization error = %v", err)
}
if _, ok := snapshot.Tools["helper.add_helper"]; !ok {
t.Fatal("manual-only command was dropped from the Catalog full-tool projection")
}
}
func TestBuildEffectiveCommandRegistryRejectsManualIdentityConflict(t *testing.T) {
root := commandRegistryTestRoot("item get")
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item get",
CanonicalPath: "item.delete_item",
Reason: "Conflict fixture",
Reviewed: true,
}},
}
_, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err == nil || !strings.Contains(err.Error(), "conflicts with command registry canonical path") {
t.Fatalf("error = %v", err)
}
}
func TestBuildEffectiveCommandRegistryRejectsPhantomManualCommand(t *testing.T) {
root := commandRegistryTestRoot("item get")
reviewed := mustCommandRegistry(t, nil)
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item missing",
CanonicalPath: "item.missing_item",
Reason: "Phantom fixture",
Reviewed: true,
}},
}
_, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err == nil || !strings.Contains(err.Error(), "does not resolve to an existing Cobra command") {
t.Fatalf("error = %v", err)
}
}
func TestBuildEffectiveCommandRegistryRejectsManualAliasCreation(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item legacy",
CanonicalPath: "item.get_item",
Reason: "Alias must be reviewed in the registry",
Reviewed: true,
}},
}
_, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err == nil || !strings.Contains(err.Error(), "cannot create an alias") {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryResolvesCompatibilityAliasLeaf(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy", "compat hidden")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
annotateTestCompatibilityPair(primary, alias)
hidden := exactSchemaCommand(root, "compat hidden")
hidden.Hidden = true
AttachRuntimeSchema(primary, "item", "get_item", "test")
AttachRuntimeSchema(alias, "item", "get_item", "test")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{
{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
},
{
CanonicalPath: "compat.hidden_helper",
PrimaryCLIPath: "compat hidden",
},
})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
item := bound.ByCanonical["item.get_item"]
if item.PrimaryCommand != primary || len(item.AliasCommands) != 1 || item.AliasCommands[0].Command != alias {
t.Fatalf("item binding = %#v", item)
}
if item.AliasCommands[0].Path != "item legacy" || item.AliasCommands[0].Kind != AliasKindCompatibilityLeaf {
t.Fatalf("compatibility alias binding = %#v", item.AliasCommands[0])
}
if bound.ByCanonical["compat.hidden_helper"].PrimaryCommand != hidden {
t.Fatal("explicit reviewed hidden runnable leaf was not bound")
}
}
func TestBindEffectiveCommandRegistryAcceptsEquivalentCompatibilityLeafContract(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
annotateTestCompatibilityPair(primary, alias)
primary.Use = "get <item-id>"
alias.Use = "legacy <item-id>"
argsValidator := cobra.ExactArgs(1)
primary.Args = argsValidator
alias.Args = argsValidator
primary.Flags().String("query", "all", "query expression (必填)")
alias.Flags().String("query", "all", "query expression (必填)")
if err := primary.MarkFlagRequired("query"); err != nil {
t.Fatalf("mark primary required: %v", err)
}
if err := alias.MarkFlagRequired("query"); err != nil {
t.Fatalf("mark alias required: %v", err)
}
AnnotateRuntimeFlagRequiredWhen(primary, "query", "item-id is present")
AnnotateRuntimeFlagRequiredWhen(alias, "query", "item-id is present")
positionals := []RuntimeSchemaPositional{{Name: "item-id", Type: "string", Required: true, Index: 0}}
AnnotateRuntimePositionals(primary, positionals...)
AnnotateRuntimePositionals(alias, positionals...)
constraints := RuntimeSchemaConstraints{RequireTogether: [][]string{{"item-id", "query"}}}
AnnotateRuntimeConstraints(primary, constraints)
AnnotateRuntimeConstraints(alias, constraints)
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
got := bound.ByCanonical["item.get_item"].AliasCommands[0]
if got.Kind != AliasKindCompatibilityLeaf || got.Command != alias {
t.Fatalf("compatibility alias = %#v", got)
}
}
func TestBindEffectiveCommandRegistryRejectsDifferentCompatibilityHandlersWithoutTypedReview(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
primary.Run = nil
alias.Run = nil
primary.RunE = compatibilityPrimaryRunE
alias.RunE = compatibilityAliasRunE
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "execution handler implementation differs for RunE") {
t.Fatalf("handler mismatch error = %v", err)
}
if !strings.Contains(err.Error(), "distinct canonical tools") {
t.Fatalf("handler mismatch error is not actionable: %v", err)
}
}
func TestBindEffectiveCommandRegistryRejectsIndependentLeafWithoutTypedReviewEvenWithSameHandler(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "independent compatibility leaves require the same reviewed typed compatibility equivalence") {
t.Fatalf("missing compatibility review error = %v", err)
}
}
func TestBindEffectiveCommandRegistryAcceptsDifferentHandlersWithMatchingTypedReview(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
primary.Run = nil
alias.Run = nil
primary.RunE = compatibilityPrimaryRunE
alias.RunE = compatibilityAliasRunE
AnnotateRuntimeCompatibilityEquivalence(primary, alias, RuntimeCompatibilityEquivalence{
ID: "item-get-legacy-v1",
Reason: "The compatibility wrapper adds presentation-only behavior before invoking the exact primary operation.",
Reviewed: true,
})
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
if _, err := BindEffectiveCommandRegistry(root, effective); err != nil {
t.Fatalf("reviewed handler equivalence was rejected: %v", err)
}
}
func TestAnnotateRuntimeCompatibilityEquivalenceRejectsConflictingExistingReview(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy", "item older")
primary := exactSchemaCommand(root, "item get")
legacy := exactSchemaCommand(root, "item legacy")
older := exactSchemaCommand(root, "item older")
AnnotateRuntimeCompatibilityEquivalence(primary, legacy, RuntimeCompatibilityEquivalence{
ID: "item-get-legacy-v1", Reason: "Reviewed first compatibility contract.", Reviewed: true,
})
defer func() {
if recovered := recover(); recovered == nil {
t.Fatal("conflicting compatibility review silently overwrote the existing marker")
}
}()
AnnotateRuntimeCompatibilityEquivalence(primary, older, RuntimeCompatibilityEquivalence{
ID: "item-get-older-v1", Reason: "Conflicting compatibility contract.", Reviewed: true,
})
}
func compatibilityPrimaryRunE(*cobra.Command, []string) error { return nil }
func compatibilityAliasRunE(*cobra.Command, []string) error { return nil }
func TestBindEffectiveCommandRegistryIgnoresLazilyMaterializedHelpFlag(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
annotateTestCompatibilityPair(primary, alias)
// Cobra does this only for the command selected by Execute. Binding after
// parsing must remain independent of whether the primary or compatibility
// path was selected first.
primary.InitDefaultHelpFlag()
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
if _, err := BindEffectiveCommandRegistry(root, effective); err != nil {
t.Fatalf("lazy Cobra --help flag changed compatibility contract: %v", err)
}
}
func TestBindEffectiveCommandRegistryCompatibilityGateIsOrderIndependentOfCanonicalMetadataMaterialization(t *testing.T) {
const canonical = "compat.get_item"
previousMetadata, hadMetadata := runtimeSchemaParameterMetadataByCanonical[canonical]
previousConstraints, hadConstraints := runtimeSchemaConstraintsByCanonical[canonical]
t.Cleanup(func() {
if hadMetadata {
runtimeSchemaParameterMetadataByCanonical[canonical] = previousMetadata
} else {
delete(runtimeSchemaParameterMetadataByCanonical, canonical)
}
if hadConstraints {
runtimeSchemaConstraintsByCanonical[canonical] = previousConstraints
} else {
delete(runtimeSchemaConstraintsByCanonical, canonical)
}
})
runtimeSchemaParameterMetadataByCanonical[canonical] = RuntimeSchemaParameterMetadata{
RequiredWhen: map[string]string{"query": "item-id is present"},
Formats: map[string]string{"query": "typed-format"},
Examples: map[string]string{"query": "open"},
Enums: map[string][]string{"query": {"open", "closed"}},
}
runtimeSchemaConstraintsByCanonical[canonical] = RuntimeSchemaConstraints{
RequireTogether: [][]string{{"item-id", "query"}},
}
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
annotateTestCompatibilityPair(primary, alias)
primary.Flags().String("query", "", "query")
alias.Flags().String("query", "", "query")
AnnotateRuntimeFlagFormat(primary, "query", "native-format")
AnnotateRuntimeFlagFormat(alias, "query", "native-format")
AnnotateRuntimeFlagExample(primary, "query", "native-example")
AnnotateRuntimeFlagExample(alias, "query", "native-example")
AnnotateRuntimeFlagEnum(primary, "query", "native-a", "native-b")
AnnotateRuntimeFlagEnum(alias, "query", "native-a", "native-b")
// Simulate a previous Schema collection pass, which materializes canonical
// metadata only on the primary leaf. Binding must model the same canonical
// facts on both paths and remain deterministic on the next pass.
applyRuntimeSchemaParameterMetadata(primary, canonical)
AnnotateRuntimeConstraints(primary, runtimeSchemaConstraintsByCanonical[canonical])
primaryFlag := primary.Flags().Lookup("query")
if got := firstFlagAnnotation(primaryFlag, "x-cli-format"); got != "native-format" {
t.Fatalf("typed metadata overwrote native format annotation: %q", got)
}
if got := firstFlagAnnotation(primaryFlag, runtimeSchemaFlagMetadataFormatAnnotation); got != "typed-format" {
t.Fatalf("typed format annotation = %q", got)
}
if got := firstFlagAnnotation(primaryFlag, runtimeSchemaFlagExampleAnnotation); got != "native-example" {
t.Fatalf("typed metadata overwrote native example annotation: %q", got)
}
if got := firstFlagAnnotation(primaryFlag, runtimeSchemaFlagMetadataExampleAnnotation); got != "open" {
t.Fatalf("typed example annotation = %q", got)
}
if got := runtimeFlagEnum(primaryFlag); !reflect.DeepEqual(got, []string{"native-a", "native-b"}) {
t.Fatalf("typed metadata overwrote native enum annotation: %#v", got)
}
if got := runtimeFlagEnumAnnotation(primaryFlag, runtimeSchemaFlagMetadataEnumAnnotation); !reflect.DeepEqual(got, []string{"open", "closed"}) {
t.Fatalf("typed enum annotation = %#v", got)
}
aliasAnnotations := effectiveCompatibilityFlagAnnotations(alias.Flags().Lookup("query"), nil, runtimeSchemaParameterMetadataByCanonical[canonical])
if got := firstCompatibilityAnnotation(aliasAnnotations, "x-cli-format"); got != "native-format" {
t.Fatalf("compatibility overlay dropped native format annotation: %q", got)
}
if got := firstCompatibilityAnnotation(aliasAnnotations, runtimeSchemaFlagMetadataFormatAnnotation); got != "typed-format" {
t.Fatalf("compatibility overlay dropped typed format annotation: %q", got)
}
if got := aliasAnnotations["x-cli-enum"]; !reflect.DeepEqual(got, []string{"native-a", "native-b"}) {
t.Fatalf("compatibility overlay dropped native enum annotation: %#v", got)
}
if got := aliasAnnotations[runtimeSchemaFlagMetadataEnumAnnotation]; !reflect.DeepEqual(got, []string{"open", "closed"}) {
t.Fatalf("compatibility overlay dropped typed enum annotation: %#v", got)
}
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: canonical,
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
if _, err := BindEffectiveCommandRegistry(root, effective); err != nil {
t.Fatalf("BindEffectiveCommandRegistry() after canonical metadata materialization error = %v", err)
}
}
func TestBindEffectiveCommandRegistryTreatsManualParameterHintsAsCanonicalProjection(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
annotateTestCompatibilityPair(primary, alias)
primary.Flags().Bool("include-archived", false, "include archived items")
alias.Flags().Bool("include-archived", false, "include archived items")
required := false
if err := annotateManualSchemaParameter(
primary,
"include-archived",
ManualSchemaParameterHint{Required: &required},
"Reviewed canonical projection may lower the Agent-facing required value.",
); err != nil {
t.Fatalf("annotateManualSchemaParameter() error = %v", err)
}
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
if _, err := BindEffectiveCommandRegistry(root, effective); err != nil {
t.Fatalf("manual canonical projection must not create compatibility-leaf executable drift: %v", err)
}
if _, _, ok, err := runtimeManualSchemaParameter(alias, "include-archived"); err != nil || ok {
t.Fatalf("manual projection was copied to compatibility leaf: present=%t err=%v", ok, err)
}
}
func TestBindEffectiveCommandRegistryRejectsCompatibilityLeafSemanticDrift(t *testing.T) {
tests := []struct {
name string
want string
mutate func(primary, alias *cobra.Command)
}{
{
name: "flag surface",
want: "flag --query is missing",
mutate: func(primary, _ *cobra.Command) {
primary.Flags().String("query", "", "query")
},
},
{
name: "flag type",
want: "flag --query type differs",
mutate: func(primary, alias *cobra.Command) {
primary.Flags().String("query", "", "query")
alias.Flags().Int("query", 0, "query")
},
},
{
name: "flag default",
want: "flag --limit default differs",
mutate: func(primary, alias *cobra.Command) {
primary.Flags().Int("limit", 20, "limit")
alias.Flags().Int("limit", 50, "limit")
},
},
{
name: "flag hidden",
want: "flag --query hidden state differs",
mutate: func(primary, alias *cobra.Command) {
primary.Flags().String("query", "", "query")
alias.Flags().String("query", "", "query")
_ = alias.Flags().MarkHidden("query")
},
},
{
name: "flag required",
want: "required/required_when facts differ",
mutate: func(primary, alias *cobra.Command) {
primary.Flags().String("query", "", "query")
alias.Flags().String("query", "", "query")
_ = primary.MarkFlagRequired("query")
},
},
{
name: "flag required when",
want: "required/required_when facts differ",
mutate: func(primary, alias *cobra.Command) {
primary.Flags().String("query", "", "query")
alias.Flags().String("query", "", "query")
AnnotateRuntimeFlagRequiredWhen(primary, "query", "mode is search")
AnnotateRuntimeFlagRequiredWhen(alias, "query", "mode is list")
},
},
{
name: "persistent behavior",
want: "local/persistent/inherited behavior differs",
mutate: func(primary, alias *cobra.Command) {
primary.PersistentFlags().String("scope", "", "scope")
alias.Flags().String("scope", "", "scope")
},
},
{
name: "Args",
want: "Args validator implementation differs",
mutate: func(primary, alias *cobra.Command) {
primary.Args = cobra.NoArgs
alias.Args = cobra.MaximumNArgs(1)
},
},
{
name: "positionals",
want: "runtime positionals differ",
mutate: func(primary, alias *cobra.Command) {
AnnotateRuntimePositionals(primary, RuntimeSchemaPositional{Name: "item-id", Required: true, Index: 0})
AnnotateRuntimePositionals(alias, RuntimeSchemaPositional{Name: "item-id", Required: false, Index: 0})
},
},
{
name: "constraints",
want: "runtime constraints differ",
mutate: func(primary, alias *cobra.Command) {
AnnotateRuntimeConstraints(primary, RuntimeSchemaConstraints{RequireOneOf: [][]string{{"query", "item-id"}}})
AnnotateRuntimeConstraints(alias, RuntimeSchemaConstraints{RequireTogether: [][]string{{"query", "item-id"}}})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
root := commandRegistryTestRoot("item get", "item legacy")
primary := exactSchemaCommand(root, "item get")
alias := exactSchemaCommand(root, "item legacy")
alias.Hidden = true
test.mutate(primary, alias)
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item legacy"},
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
if !strings.Contains(err.Error(), "item.get_item") || !strings.Contains(err.Error(), `compatibility leaf alias "item legacy"`) {
t.Fatalf("error is not actionable: %v", err)
}
})
}
}
func TestBindEffectiveCommandRegistryResolvesAncestorCobraAlias(t *testing.T) {
root := commandRegistryTestRoot("item action get")
group := exactSchemaCommand(root, "item action")
group.Aliases = []string{"ops"}
primary := exactSchemaCommand(root, "item action get")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item action get",
Aliases: []string{"item ops get"},
}})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
alias := bound.ByCanonical["item.get_item"].AliasCommands[0]
if alias.Command != primary || alias.Kind != AliasKindCobraAlias {
t.Fatalf("ancestor Cobra alias binding = %#v", alias)
}
}
func TestBindEffectiveCommandRegistryRejectsAmbiguousCobraAlias(t *testing.T) {
root := commandRegistryTestRoot("item get", "item list")
exactSchemaCommand(root, "item get").Aliases = []string{"fetch"}
exactSchemaCommand(root, "item list").Aliases = []string{"fetch"}
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item fetch"},
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), `cobra alias segment "fetch" is ambiguous`) {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryPrefersExactNameOverCobraAlias(t *testing.T) {
root := commandRegistryTestRoot("item get", "item fetch")
primary := exactSchemaCommand(root, "item get")
compatibility := exactSchemaCommand(root, "item fetch")
primary.Aliases = []string{"fetch"}
compatibility.Hidden = true
annotateTestCompatibilityPair(primary, compatibility)
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item fetch"},
}})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
alias := bound.ByCanonical["item.get_item"].AliasCommands[0]
if alias.Command != compatibility || alias.Kind != AliasKindCompatibilityLeaf {
t.Fatalf("name-priority alias binding = %#v", alias)
}
}
func TestBindEffectiveCommandRegistryResolvesCobraAliasToPrimaryCommand(t *testing.T) {
root := commandRegistryTestRoot("item get")
primary := exactSchemaCommand(root, "item get")
primary.Aliases = []string{"fetch"}
AttachRuntimeSchema(primary, "item", "get_item", "test")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item fetch"},
}})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
alias := bound.ByCanonical["item.get_item"].AliasCommands[0]
if alias.Path != "item fetch" || alias.Command != primary || alias.Kind != AliasKindCobraAlias {
t.Fatalf("Cobra alias binding = %#v", alias)
}
if got := bound.ByCLIPath["item fetch"].CanonicalPath; got != "item.get_item" {
t.Fatalf("alias canonical = %q", got)
}
}
func TestBindEffectiveCommandRegistryRejectsCobraAliasToDifferentPrimary(t *testing.T) {
root := commandRegistryTestRoot("item get", "item list")
list := exactSchemaCommand(root, "item list")
list.Aliases = []string{"fetch"}
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item fetch"},
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "resolves to a different command") {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryRejectsAliasAsPrimaryPath(t *testing.T) {
root := commandRegistryTestRoot("item get")
exactSchemaCommand(root, "item get").Aliases = []string{"fetch"}
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item fetch",
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "primary path") || !strings.Contains(err.Error(), "real Cobra command names") {
t.Fatalf("error = %v", err)
}
}
func TestBuildEffectiveCommandRegistryResolvesManualAliasBeforeRegistryPolicy(t *testing.T) {
root := commandRegistryTestRoot("item action get")
exactSchemaCommand(root, "item action").Aliases = []string{"ops"}
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item action get",
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item ops get",
CanonicalPath: "item.get_item",
Reason: "Reviewed ancestor Cobra alias",
Reviewed: true,
}},
}
_, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err == nil || !strings.Contains(err.Error(), "cannot create an alias") {
t.Fatalf("error = %v", err)
}
}
func TestBuildEffectiveCommandRegistryRejectsAmbiguousManualAlias(t *testing.T) {
root := commandRegistryTestRoot("item get", "item list")
exactSchemaCommand(root, "item get").Aliases = []string{"fetch"}
exactSchemaCommand(root, "item list").Aliases = []string{"fetch"}
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item fetch",
CanonicalPath: "item.get_item",
Reason: "Ambiguous alias fixture",
Reviewed: true,
}},
}
_, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err == nil || !strings.Contains(err.Error(), `cobra alias segment "fetch" is ambiguous`) {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryRejectsStaleRegistryPath(t *testing.T) {
root := commandRegistryTestRoot("item get")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.delete_item",
PrimaryCLIPath: "item delete",
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "stale cli path") {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryDoesNotPrefixMatch(t *testing.T) {
root := commandRegistryTestRoot("item get")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item ge",
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), `stale cli path "item ge"`) {
t.Fatalf("error = %v", err)
}
}
func TestBindEffectiveCommandRegistryRejectsNativeAnnotationConflict(t *testing.T) {
root := commandRegistryTestRoot("item get")
leaf := exactSchemaCommand(root, "item get")
AttachRuntimeSchema(leaf, "item", "delete_item", "test")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
_, err := BindEffectiveCommandRegistry(root, effective)
if err == nil || !strings.Contains(err.Error(), "conflicts with native annotation item.delete_item") {
t.Fatalf("error = %v", err)
}
}
func TestCommandRegistryRejectsAliasCollision(t *testing.T) {
_, err := newEffectiveCommandRegistry([]CommandSpec{
{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
Aliases: []string{"item shared"},
},
{
CanonicalPath: "item.delete_item",
PrimaryCLIPath: "item delete",
Aliases: []string{"item shared"},
},
})
if err == nil || !strings.Contains(err.Error(), "belongs to both") {
t.Fatalf("error = %v", err)
}
}
func TestCommandRegistryDefaultsAndValidatesReviewedVisibility(t *testing.T) {
registry := mustEffectiveCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
if got := registry.ByCanonical["item.get_item"].Visibility; got != SchemaVisibilityPublic {
t.Fatalf("default visibility = %q, want public", got)
}
_, err := newEffectiveCommandRegistry([]CommandSpec{{
CanonicalPath: "item.delete_item",
PrimaryCLIPath: "item delete",
Visibility: SchemaVisibility("unreviewed"),
}})
if err == nil || !strings.Contains(err.Error(), "invalid visibility") {
t.Fatalf("invalid visibility error = %v", err)
}
}
func TestAssemblerUsesReviewedCommandVisibility(t *testing.T) {
root := commandRegistryTestRoot("doc-comment run", "item internal")
effective := mustEffectiveCommandRegistry(t, []CommandSpec{
{
CanonicalPath: "doc-comment.run",
PrimaryCLIPath: "doc-comment run",
Visibility: SchemaVisibilityPublic,
},
{
CanonicalPath: "item.internal",
PrimaryCLIPath: "item internal",
Visibility: SchemaVisibilityInternal,
},
})
bound, err := BindEffectiveCommandRegistry(root, effective)
if err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
registry, err := AssembleSchemaRegistryFromBound(bound)
if err != nil {
t.Fatalf("AssembleSchemaRegistryFromBound() error = %v", err)
}
index, err := registry.Index()
if err != nil {
t.Fatalf("SchemaRegistry.Index() error = %v", err)
}
if _, ok := index.Resolve("doc-comment.run"); !ok {
t.Fatal("reviewed public command was filtered by legacy product visibility")
}
if _, ok := index.Resolve("item.internal"); ok {
t.Fatal("reviewed internal command was emitted by the assembler")
}
}
func TestCommandRegistrySourceValidationIsFailClosed(t *testing.T) {
registry, err := ValidateCommandRegistrySource(embeddedSchemaCommandRegistryJSON)
if err != nil {
t.Fatalf("ValidateCommandRegistrySource(embedded) error = %v", err)
}
hash, err := EmbeddedCommandRegistrySourceHash()
if err != nil {
t.Fatalf("EmbeddedCommandRegistrySourceHash() error = %v", err)
}
if hash == "" || hash != registry.SourceHash() {
t.Fatalf("embedded hash = %q, decoded hash = %q", hash, registry.SourceHash())
}
drifted := strings.Replace(string(embeddedSchemaCommandRegistryJSON), `"cli_path": "aisearch person"`, `"cli_path": "aisearch people"`, 1)
if drifted == string(embeddedSchemaCommandRegistryJSON) {
t.Fatal("test fixture did not mutate embedded registry")
}
_, err = ValidateCommandRegistrySource([]byte(drifted))
if err == nil || !strings.Contains(err.Error(), "disagrees with the embedded reviewed registry") {
t.Fatalf("drift error = %v", err)
}
unknownField := strings.Replace(string(embeddedSchemaCommandRegistryJSON), `"version": 1`, `"version": 1, "unreviewed": true`, 1)
_, err = ValidateCommandRegistrySource([]byte(unknownField))
if err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("unknown-field error = %v", err)
}
}
func commandRegistryTestRoot(paths ...string) *cobra.Command {
root := &cobra.Command{Use: "dws"}
groups := map[string]*cobra.Command{}
for _, path := range paths {
parts := strings.Fields(path)
current := root
prefix := ""
for idx, part := range parts {
if prefix == "" {
prefix = part
} else {
prefix += " " + part
}
if idx == len(parts)-1 {
current.AddCommand(&cobra.Command{Use: part, Run: func(*cobra.Command, []string) {}})
continue
}
next := groups[prefix]
if next == nil {
next = &cobra.Command{Use: part}
groups[prefix] = next
current.AddCommand(next)
}
current = next
}
}
return root
}
func mustCommandRegistry(t *testing.T, commands []CommandSpec) CommandRegistry {
t.Helper()
registry, err := newCommandRegistry(commands)
if err != nil {
t.Fatalf("newCommandRegistry() error = %v", err)
}
return registry
}
func mustEffectiveCommandRegistry(t *testing.T, commands []CommandSpec) EffectiveCommandRegistry {
t.Helper()
registry, err := newEffectiveCommandRegistry(commands)
if err != nil {
t.Fatalf("newEffectiveCommandRegistry() error = %v", err)
}
return registry
}
func annotateTestCompatibilityPair(primary, alias *cobra.Command) {
AnnotateRuntimeCompatibilityEquivalence(primary, alias, RuntimeCompatibilityEquivalence{
ID: "test-compatibility-pair-v1",
Reason: "The focused test explicitly reviews these independently registered leaves as semantically equivalent.",
Reviewed: true,
})
}
+161
View File
@@ -0,0 +1,161 @@
{
"version": 1,
"groups": [
{
"id": "cli-management",
"reason": "Local CLI lifecycle, authentication, configuration, recovery, and plugin-management commands are user-operated controls rather than stable Agent tools.",
"reviewed": true,
"commands": [
"api",
"auth export",
"auth import",
"auth login",
"auth logout",
"auth migrate-keychain",
"auth reset",
"auth status",
"completion",
"config list",
"dev connect list",
"dev connect restart",
"doctor",
"plugin build",
"plugin config get",
"plugin config list",
"plugin config set",
"plugin config unset",
"plugin create",
"plugin dev",
"plugin disable",
"plugin enable",
"plugin info",
"plugin install",
"plugin list",
"plugin remove",
"plugin validate",
"profile list",
"profile switch",
"profile use",
"recovery execute",
"recovery finalize",
"recovery plan",
"schema",
"skill get",
"skill install",
"skill search",
"skill setup",
"upgrade",
"version"
]
},
{
"id": "agoal-out-of-surface",
"reason": "The Agoal product remains executable for compatibility but is outside the currently reviewed open-source Agent command surface.",
"reviewed": true,
"commands": [
"agoal contract detail",
"agoal contract fields",
"agoal contract list",
"agoal contract update",
"agoal obj-template create-or-update",
"agoal obj-template list",
"agoal report list-statistics",
"agoal report submit-detail",
"agoal scorecard detail",
"agoal scorecard entity-detail",
"agoal scorecard update",
"agoal strategy detail",
"agoal strategy list",
"agoal strategy update",
"agoal user objectives",
"agoal user rules"
]
},
{
"id": "compatibility-helpers-pending-review",
"reason": "This executable compatibility/helper command has not yet completed stable interface, parameter, and Agent safety review; it must be reviewed before entering the release Catalog.",
"reviewed": true,
"commands": [
"calendar acl add",
"calendar acl delete",
"calendar book update",
"chat category add-conv",
"chat category create",
"chat category delete",
"chat category remove-conv",
"chat category rename",
"chat chmod",
"chat clear-all-red-point",
"chat clear-messages",
"chat clear-red-point",
"chat data-auth cross-org",
"chat group audit-join-validation",
"chat group list-all",
"chat group list-join-validations",
"chat group members list-by-ids",
"chat group notice create",
"chat group notice edit",
"chat group notice get",
"chat group notice list",
"chat group share-invite",
"chat group update-alias",
"chat group update-nick",
"chat hide",
"chat list-all-conversations",
"chat mark-read",
"chat mark-unread",
"chat media upload",
"chat message list-emotion-replies",
"chat message set-top-msg",
"chat message unset-top-msg",
"chat mute-at-all",
"chat mute-red-envelope",
"chat text translate",
"contact label get",
"contact label list",
"contact label list-members",
"dev app version check-approval",
"ding message list",
"ding message recall-personal",
"ding message receiver-status",
"ding message send-by-message",
"ding message send-personal",
"mail allow-list add",
"mail allow-list list",
"mail allow-list remove",
"mail auto-reply get",
"mail auto-reply update",
"mail block-list add",
"mail block-list list",
"mail block-list remove",
"mail mailbox profile",
"mail message batch-get",
"mail message batch-update",
"mail message verify",
"mail rule adjust",
"mail rule create",
"mail rule delete",
"mail rule list",
"mail rule update",
"mail sent-message recall",
"mail sent-message recall-detail",
"mail tag create",
"mail tag delete",
"mail tag update",
"mail thread batch-trash",
"mail thread batch-update",
"mail thread list",
"mail thread trash",
"mail thread update",
"oa approval append-task",
"oa approval ding-info",
"oa approval revert-activities",
"oa approval revert-task",
"oa approval search-forms",
"todo task list-attachment",
"todo task list-sub",
"todo task remove-attachment"
]
}
]
}
+519
View File
@@ -0,0 +1,519 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli
import (
"bytes"
"crypto/sha256"
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"regexp"
"sort"
"strings"
"sync"
"github.com/spf13/cobra"
)
const commandRegistrySchemaRef = "./schema_command_registry.schema.json"
// schema_command_registry.json is the reviewed, typed command registry and the
// sole source of stable command identity and navigation. Catalog and generated
// metadata are downstream views and must never be read back here.
//go:embed schema_command_registry.json
var embeddedSchemaCommandRegistryJSON []byte
//go:embed schema_command_registry.schema.json
var embeddedSchemaCommandRegistrySchemaJSON []byte
var (
commandRegistryProductIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]*$`)
commandRegistryCanonicalPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9][A-Za-z0-9_.-]*$`)
commandRegistryCLIPathToken = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]*$`)
)
type schemaCommandRegistrySnapshot struct {
Schema string `json:"$schema,omitempty"`
Version int `json:"version"`
Products []schemaCommandRegistryProduct `json:"products"`
}
type schemaCommandRegistryProduct struct {
ID string `json:"id"`
Tools []schemaCommandRegistryTool `json:"tools"`
}
type schemaCommandRegistryTool struct {
CanonicalPath string `json:"canonical_path"`
SourceProductID *string `json:"source_product_id,omitempty"`
CLIPath string `json:"cli_path"`
Aliases []string `json:"aliases,omitempty"`
Visibility *SchemaVisibility `json:"visibility,omitempty"`
}
// CommandSpec is one reviewed command identity. Identity and navigation are
// deliberately kept together so no downstream renderer can independently
// invent a canonical name, primary path, or alias.
type CommandSpec struct {
CanonicalPath string
SourceProductID string
PrimaryCLIPath string
Aliases []string
Visibility SchemaVisibility
Source string
ReviewReason string
}
// CommandRegistry is the decoded reviewed identity registry.
type CommandRegistry struct {
Commands []CommandSpec
ByCLIPath map[string]CommandSpec
ByCanonical map[string]CommandSpec
}
// EffectiveCommandRegistry is the reviewed registry after exact reviewed
// manual command additions have been merged. It remains independent of Cobra;
// binding is a separate fail-closed step.
type EffectiveCommandRegistry struct {
Commands []CommandSpec
ByCLIPath map[string]CommandSpec
ByCanonical map[string]CommandSpec
}
var (
embeddedSchemaCommandRegistryOnce sync.Once
embeddedSchemaCommandRegistryData CommandRegistry
embeddedSchemaCommandRegistryErr error
)
func loadEmbeddedCommandRegistry() (CommandRegistry, error) {
embeddedSchemaCommandRegistryOnce.Do(func() {
embeddedSchemaCommandRegistryData, embeddedSchemaCommandRegistryErr = decodeCommandRegistry(embeddedSchemaCommandRegistryJSON)
})
return cloneCommandRegistry(embeddedSchemaCommandRegistryData), embeddedSchemaCommandRegistryErr
}
// ValidateCommandRegistrySource validates a compatibility --surface input and
// requires it to be semantically identical to the embedded reviewed registry.
// Generators may retain the flag for migration, but cannot use it to introduce
// a second identity source.
func ValidateCommandRegistrySource(data []byte) (CommandRegistry, error) {
candidate, err := decodeCommandRegistry(data)
if err != nil {
return CommandRegistry{}, err
}
embedded, err := loadEmbeddedCommandRegistry()
if err != nil {
return CommandRegistry{}, err
}
if !equalCommandRegistries(candidate, embedded) {
return CommandRegistry{}, fmt.Errorf("command registry source disagrees with the embedded reviewed registry")
}
return candidate, nil
}
// EmbeddedCommandRegistrySourceHash returns the stable semantic hash used by
// all generated downstream views.
func EmbeddedCommandRegistrySourceHash() (string, error) {
registry, err := loadEmbeddedCommandRegistry()
if err != nil {
return "", err
}
return registry.SourceHash(), nil
}
// SourceHash hashes only stable identity, navigation, and reviewed exposure.
// Formatting, product order, provenance labels, and omitted default
// source_product_id/visibility values do not affect it.
func (registry CommandRegistry) SourceHash() string {
return hashCommandSpecs(registry.Commands)
}
// SourceHash includes reviewed manual additions because those entries are part
// of the effective identity registry delivered to downstream consumers.
func (registry EffectiveCommandRegistry) SourceHash() string {
return hashCommandSpecs(registry.Commands)
}
func hashCommandSpecs(commands []CommandSpec) string {
rows := make([]string, 0, len(commands))
for _, spec := range commands {
productID, _, _ := splitManualSchemaCanonicalPath(spec.CanonicalPath)
sourceProductID := strings.TrimSpace(spec.SourceProductID)
if sourceProductID == productID {
sourceProductID = ""
}
aliases := normalizeCommandAliases(spec.Aliases, normalizeSchemaCLIPath(spec.PrimaryCLIPath))
visibility := spec.Visibility
if visibility == "" {
visibility = SchemaVisibilityPublic
}
rows = append(rows, productID+"\x00"+strings.TrimSpace(spec.CanonicalPath)+"\x00"+sourceProductID+"\x00"+normalizeSchemaCLIPath(spec.PrimaryCLIPath)+"\x00"+strings.Join(aliases, "\x00")+"\x00"+string(visibility))
}
sort.Strings(rows)
sum := sha256.Sum256([]byte(strings.Join(rows, "\n")))
return "sha256:" + hex.EncodeToString(sum[:])
}
func equalCommandRegistries(left, right CommandRegistry) bool {
if len(left.Commands) != len(right.Commands) || left.SourceHash() != right.SourceHash() {
return false
}
for canonical, leftSpec := range left.ByCanonical {
rightSpec, ok := right.ByCanonical[canonical]
if !ok || leftSpec.SourceProductID != rightSpec.SourceProductID || leftSpec.PrimaryCLIPath != rightSpec.PrimaryCLIPath || strings.Join(leftSpec.Aliases, "\x00") != strings.Join(rightSpec.Aliases, "\x00") || leftSpec.Visibility != rightSpec.Visibility {
return false
}
}
return true
}
func decodeCommandRegistry(data []byte) (CommandRegistry, error) {
var snapshot schemaCommandRegistrySnapshot
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&snapshot); err != nil {
return CommandRegistry{}, fmt.Errorf("decode reviewed Schema command registry: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
err = fmt.Errorf("multiple JSON values")
}
return CommandRegistry{}, fmt.Errorf("decode reviewed Schema command registry: %w", err)
}
if snapshot.Version != 1 {
return CommandRegistry{}, fmt.Errorf("unsupported Schema command registry version %d", snapshot.Version)
}
if strings.TrimSpace(snapshot.Schema) != commandRegistrySchemaRef {
return CommandRegistry{}, fmt.Errorf("schema command registry must declare $schema=%q", commandRegistrySchemaRef)
}
if len(snapshot.Products) == 0 {
return CommandRegistry{}, fmt.Errorf("schema command registry contains no products")
}
products := append([]schemaCommandRegistryProduct(nil), snapshot.Products...)
sort.Slice(products, func(i, j int) bool { return products[i].ID < products[j].ID })
commands := make([]CommandSpec, 0)
seenProducts := make(map[string]bool, len(products))
for _, product := range products {
productID := strings.TrimSpace(product.ID)
if productID != product.ID || !validCommandRegistryProductID(productID) {
return CommandRegistry{}, fmt.Errorf("schema command registry contains invalid product id %q", product.ID)
}
if seenProducts[productID] {
return CommandRegistry{}, fmt.Errorf("schema command registry contains duplicate product id %q", productID)
}
seenProducts[productID] = true
if len(product.Tools) == 0 {
return CommandRegistry{}, fmt.Errorf("schema command registry product %s contains no commands", productID)
}
for _, tool := range product.Tools {
canonical := strings.TrimSpace(tool.CanonicalPath)
canonicalProduct, _, ok := splitManualSchemaCanonicalPath(canonical)
if canonical != tool.CanonicalPath || !ok || !commandRegistryCanonicalPattern.MatchString(canonical) || canonicalProduct != productID {
return CommandRegistry{}, fmt.Errorf("schema command registry product %s contains invalid canonical path %q", productID, canonical)
}
sourceProductID := productID
if tool.SourceProductID != nil {
rawSourceProductID := *tool.SourceProductID
sourceProductID = strings.TrimSpace(rawSourceProductID)
if sourceProductID != rawSourceProductID || !validCommandRegistryProductID(sourceProductID) {
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s has invalid source_product_id %q", canonical, rawSourceProductID)
}
}
if sourceProductID == "" {
sourceProductID = productID
}
if !validReviewedCommandRegistryCLIPath(tool.CLIPath) {
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s has invalid primary cli path %q", canonical, tool.CLIPath)
}
seenAliases := make(map[string]bool, len(tool.Aliases))
for _, alias := range tool.Aliases {
if !validReviewedCommandRegistryCLIPath(alias) {
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s has invalid alias path %q", canonical, alias)
}
if alias == tool.CLIPath {
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s alias %q duplicates its primary cli path", canonical, alias)
}
if seenAliases[alias] {
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s contains duplicate alias %q", canonical, alias)
}
seenAliases[alias] = true
}
visibility := SchemaVisibilityPublic
if tool.Visibility != nil {
visibility = *tool.Visibility
switch visibility {
case SchemaVisibilityPublic, SchemaVisibilityCompat, SchemaVisibilityInternal:
default:
return CommandRegistry{}, fmt.Errorf("schema command registry tool %s has invalid visibility %q", canonical, visibility)
}
}
commands = append(commands, CommandSpec{
CanonicalPath: canonical,
SourceProductID: sourceProductID,
PrimaryCLIPath: tool.CLIPath,
Aliases: tool.Aliases,
Visibility: visibility,
Source: "reviewed_command_registry",
})
}
}
return newCommandRegistry(commands)
}
func newCommandRegistry(commands []CommandSpec) (CommandRegistry, error) {
normalized, byPath, byCanonical, err := indexCommandSpecs(commands)
if err != nil {
return CommandRegistry{}, err
}
return CommandRegistry{Commands: normalized, ByCLIPath: byPath, ByCanonical: byCanonical}, nil
}
func newEffectiveCommandRegistry(commands []CommandSpec) (EffectiveCommandRegistry, error) {
normalized, byPath, byCanonical, err := indexCommandSpecs(commands)
if err != nil {
return EffectiveCommandRegistry{}, err
}
return EffectiveCommandRegistry{Commands: normalized, ByCLIPath: byPath, ByCanonical: byCanonical}, nil
}
func indexCommandSpecs(commands []CommandSpec) ([]CommandSpec, map[string]CommandSpec, map[string]CommandSpec, error) {
normalized := make([]CommandSpec, 0, len(commands))
byPath := make(map[string]CommandSpec, len(commands))
byCanonical := make(map[string]CommandSpec, len(commands))
for _, raw := range commands {
spec := cloneCommandSpec(raw)
spec.CanonicalPath = strings.TrimSpace(spec.CanonicalPath)
productID, _, ok := splitManualSchemaCanonicalPath(spec.CanonicalPath)
if !ok || !commandRegistryCanonicalPattern.MatchString(spec.CanonicalPath) {
return nil, nil, nil, fmt.Errorf("schema command registry contains invalid canonical path %q", raw.CanonicalPath)
}
spec.SourceProductID = strings.TrimSpace(spec.SourceProductID)
if spec.SourceProductID == "" {
spec.SourceProductID = productID
}
if !validCommandRegistryProductID(spec.SourceProductID) {
return nil, nil, nil, fmt.Errorf("schema command registry tool %s has invalid source_product_id %q", spec.CanonicalPath, raw.SourceProductID)
}
spec.PrimaryCLIPath = normalizeSchemaCLIPath(spec.PrimaryCLIPath)
if !validReviewedCommandRegistryCLIPath(spec.PrimaryCLIPath) {
return nil, nil, nil, fmt.Errorf("schema command registry tool %s has invalid primary cli path %q", spec.CanonicalPath, raw.PrimaryCLIPath)
}
aliases := make([]string, 0, len(spec.Aliases))
seenAliases := make(map[string]bool, len(spec.Aliases))
for _, rawAlias := range spec.Aliases {
alias := normalizeSchemaCLIPath(rawAlias)
if !validReviewedCommandRegistryCLIPath(alias) {
return nil, nil, nil, fmt.Errorf("schema command registry tool %s has invalid alias path %q", spec.CanonicalPath, alias)
}
if alias == spec.PrimaryCLIPath {
return nil, nil, nil, fmt.Errorf("schema command registry tool %s alias %q duplicates its primary path", spec.CanonicalPath, alias)
}
if seenAliases[alias] {
return nil, nil, nil, fmt.Errorf("schema command registry tool %s has duplicate alias path %q", spec.CanonicalPath, alias)
}
seenAliases[alias] = true
aliases = append(aliases, alias)
}
spec.Aliases = sortedUniqueStrings(aliases)
if spec.Visibility == "" {
spec.Visibility = SchemaVisibilityPublic
}
switch spec.Visibility {
case SchemaVisibilityPublic, SchemaVisibilityCompat, SchemaVisibilityInternal:
default:
return nil, nil, nil, fmt.Errorf("schema command registry tool %s has invalid visibility %q", spec.CanonicalPath, spec.Visibility)
}
spec.Source = strings.TrimSpace(spec.Source)
if spec.Source == "" {
spec.Source = "reviewed_command_registry"
}
spec.ReviewReason = strings.TrimSpace(spec.ReviewReason)
if previous, exists := byCanonical[spec.CanonicalPath]; exists {
return nil, nil, nil, fmt.Errorf("duplicate Schema command registry canonical path %s (primary paths %q and %q)", spec.CanonicalPath, previous.PrimaryCLIPath, spec.PrimaryCLIPath)
}
byCanonical[spec.CanonicalPath] = spec
for _, path := range append([]string{spec.PrimaryCLIPath}, spec.Aliases...) {
if previous, exists := byPath[path]; exists {
return nil, nil, nil, fmt.Errorf("schema command registry path %q belongs to both %s and %s", path, previous.CanonicalPath, spec.CanonicalPath)
}
byPath[path] = spec
}
normalized = append(normalized, spec)
}
for path, owner := range byPath {
if canonicalOwner, exists := byCanonical[path]; exists {
return nil, nil, nil, fmt.Errorf(
"schema command registry CLI path %q for %s conflicts with canonical identity %s",
path,
owner.CanonicalPath,
canonicalOwner.CanonicalPath,
)
}
}
sort.Slice(normalized, func(i, j int) bool { return normalized[i].CanonicalPath < normalized[j].CanonicalPath })
return normalized, byPath, byCanonical, nil
}
func validCommandRegistryProductID(value string) bool {
return commandRegistryProductIDPattern.MatchString(strings.TrimSpace(value))
}
// validReviewedCommandRegistryCLIPath is intentionally stricter than
// normalizeSchemaCLIPath: reviewed source must already be canonical and may
// not rely on normalization to hide a leading dws, repeated whitespace,
// flags, or wildcard syntax.
func validReviewedCommandRegistryCLIPath(value string) bool {
if value == "" || value != strings.TrimSpace(value) || strings.HasPrefix(value, "dws ") || strings.ContainsAny(value, "*?[]") {
return false
}
parts := strings.Split(value, " ")
if len(parts) == 0 {
return false
}
for _, part := range parts {
if !commandRegistryCLIPathToken.MatchString(part) {
return false
}
}
return true
}
func normalizeCommandAliases(aliases []string, primary string) []string {
normalized := make([]string, 0, len(aliases))
for _, alias := range aliases {
alias = normalizeSchemaCLIPath(alias)
if alias != "" && alias != primary {
normalized = append(normalized, alias)
}
}
return sortedUniqueStrings(normalized)
}
// BuildEffectiveCommandRegistry loads the reviewed registry and merges the
// embedded manual additions. Manual entries may only name an exact existing
// public runnable leaf. They may add a new command identity, but can never
// rewrite an existing registry identity or create an alias; aliases belong in
// the reviewed base CommandRegistry.
func BuildEffectiveCommandRegistry(root *cobra.Command) (EffectiveCommandRegistry, error) {
if root == nil {
return EffectiveCommandRegistry{}, fmt.Errorf("build effective Schema command registry: root is nil")
}
if err := ValidateEmbeddedSchemaParameterBindings(); err != nil {
return EffectiveCommandRegistry{}, fmt.Errorf("validate reviewed Schema parameter bindings: %w", err)
}
reviewed, err := loadEmbeddedCommandRegistry()
if err != nil {
return EffectiveCommandRegistry{}, err
}
manual, err := embeddedManualSchemaHints()
if err != nil {
return EffectiveCommandRegistry{}, err
}
return buildEffectiveCommandRegistry(root, reviewed, manual)
}
func buildEffectiveCommandRegistry(root *cobra.Command, reviewed CommandRegistry, manual ManualSchemaHintSnapshot) (EffectiveCommandRegistry, error) {
if root == nil {
return EffectiveCommandRegistry{}, fmt.Errorf("build effective Schema command registry: root is nil")
}
if manual.Version != manualSchemaHintVersion {
return EffectiveCommandRegistry{}, fmt.Errorf("unsupported manual Schema hint version %d", manual.Version)
}
base, err := newCommandRegistry(reviewed.Commands)
if err != nil {
return EffectiveCommandRegistry{}, err
}
commands := append([]CommandSpec(nil), base.Commands...)
byCanonical := base.ByCanonical
byPath := base.ByCLIPath
seenManualPaths := map[string]bool{}
for _, raw := range manual.Commands {
path := normalizeSchemaCLIPath(raw.CLIPath)
canonical := strings.TrimSpace(raw.CanonicalPath)
reason := strings.TrimSpace(raw.Reason)
if path == "" || strings.ContainsAny(path, "*?[]") {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint has invalid exact cli_path %q", raw.CLIPath)
}
if seenManualPaths[path] {
return EffectiveCommandRegistry{}, fmt.Errorf("duplicate manual Schema hint for %q", path)
}
seenManualPaths[path] = true
if !raw.Reviewed || reason == "" {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q is not reviewed or has no reason", path)
}
productID, _, ok := splitManualSchemaCanonicalPath(canonical)
if !ok {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q has invalid canonical_path %q", path, canonical)
}
match, resolveErr := resolveExactCobraPath(root, path)
if resolveErr != nil {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q cannot be resolved exactly: %w", path, resolveErr)
}
command := match.Command
if command == nil {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q does not resolve to an existing Cobra command", path)
}
if !publicRunnableSchemaLeaf(command) {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q must target a public runnable Cobra leaf", path)
}
namePath := normalizeSchemaCLIPath(strings.Join(commandPathParts(command), " "))
if match.UsedAlias {
nameSpec, nameExists := byPath[namePath]
if !nameExists {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q uses a Cobra alias, but real command path %q is not present in reviewed CommandRegistry", path, namePath)
}
if nameSpec.CanonicalPath != canonical {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q canonical path %q conflicts with real command path %q canonical path %q", path, canonical, namePath, nameSpec.CanonicalPath)
}
}
pathSpec, pathExists := byPath[path]
canonicalSpec, canonicalExists := byCanonical[canonical]
if pathExists && pathSpec.CanonicalPath != canonical {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q canonical path %q conflicts with command registry canonical path %q", path, canonical, pathSpec.CanonicalPath)
}
if pathExists && canonicalExists && pathSpec.CanonicalPath != canonicalSpec.CanonicalPath {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q has inconsistent command registry identity %q", path, canonical)
}
if pathExists {
continue
}
if canonicalExists {
return EffectiveCommandRegistry{}, fmt.Errorf("manual Schema hint %q cannot create an alias for %s; add the alias to the reviewed CommandRegistry", path, canonicalSpec.CanonicalPath)
}
commands = append(commands, CommandSpec{
CanonicalPath: canonical,
SourceProductID: productID,
PrimaryCLIPath: path,
Visibility: SchemaVisibilityPublic,
Source: "reviewed_manual_hint",
ReviewReason: reason,
})
// Update the working indexes so later manual entries cannot collide.
added := commands[len(commands)-1]
byCanonical[canonical] = added
byPath[path] = added
}
return newEffectiveCommandRegistry(commands)
}
func cloneCommandRegistry(registry CommandRegistry) CommandRegistry {
clone, err := newCommandRegistry(registry.Commands)
if err != nil {
return CommandRegistry{}
}
return clone
}
func cloneCommandSpec(spec CommandSpec) CommandSpec {
spec.Aliases = append([]string(nil), spec.Aliases...)
return spec
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,105 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/blob/main/internal/cli/schema_command_registry.schema.json",
"title": "DWS Reviewed Command Registry",
"description": "Human-reviewed source of stable Schema command identity and navigation. Generators validate this file and derive downstream assets from it; they must never rewrite it.",
"type": "object",
"additionalProperties": false,
"required": [
"$schema",
"version",
"products"
],
"properties": {
"$schema": {
"const": "./schema_command_registry.schema.json",
"description": "Relative editor contract. Keep this value unchanged so agents can validate the registry without network access."
},
"version": {
"const": 1,
"description": "CommandRegistry source format version."
},
"products": {
"type": "array",
"minItems": 1,
"description": "Reviewed product partitions. Go validation additionally rejects duplicate product ids, canonical identities, primary paths, and aliases.",
"items": {
"$ref": "#/$defs/product"
}
}
},
"$defs": {
"productId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$",
"description": "Stable Schema product id."
},
"cliPath": {
"type": "string",
"pattern": "^(?!dws(?:\\s|$))(?!.*(?:^|\\s)--)(?!.*[*?\\[\\]])[A-Za-z0-9][A-Za-z0-9._:-]*(?: [A-Za-z0-9][A-Za-z0-9._:-]*)*$",
"description": "Exact Cobra command path without the leading 'dws', flags, wildcard characters, or surrounding/repeated whitespace."
},
"product": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"tools"
],
"properties": {
"id": {
"$ref": "#/$defs/productId"
},
"tools": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/commandSpec"
}
}
}
},
"commandSpec": {
"type": "object",
"additionalProperties": false,
"required": [
"canonical_path",
"cli_path"
],
"properties": {
"canonical_path": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*\\.[A-Za-z0-9][A-Za-z0-9_.-]*$",
"description": "Stable product.tool identity. Its product prefix must equal the containing product id; Go validation enforces that cross-field constraint."
},
"source_product_id": {
"$ref": "#/$defs/productId",
"description": "Optional implementation product id when it differs from the canonical product. Omission means the containing product id."
},
"cli_path": {
"$ref": "#/$defs/cliPath",
"description": "Reviewed primary CLI path stored as CommandSpec.PrimaryCLIPath. It must bind exactly to one runnable Cobra leaf."
},
"aliases": {
"type": "array",
"uniqueItems": true,
"default": [],
"description": "Reviewed compatibility paths for the same identity. An alias cannot equal the primary path or collide with any other command path.",
"items": {
"$ref": "#/$defs/cliPath"
}
},
"visibility": {
"type": "string",
"enum": [
"public",
"compat",
"internal"
],
"default": "public",
"description": "Reviewed exposure. Omission is normalized to public. public enters the Agent Schema; compat/internal remain explicit registry entries but are not public delivery tools."
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More