- drop shareScope.required=["linkShare"] from the result schema: the
production response omits linkShare unless link sharing is configured;
document the conditional presence in the schema description, tests,
and both skill references
- spell out policy value tiers in skill docs (READER_AND_ABOVE /
DOWNLOADER_AND_ABOVE / EDITOR_AND_ABOVE / MANAGER_AND_ABOVE) so the
shorthand no longer diverges from the schema enums
Sync the get-setting schema descriptions and skill references with the
server-side finalized policy texts: policy code semantics now quote the
product permission-settings page labels (e.g. external_share=add
external collaborators, watermark=show watermark, node_spread_scope=
download and distribution scope), the node_spread_scope binary values
read ALL_NODES=all documents and PREVIEWABLE_ONLY=previewable documents
only (online documents, images, videos, etc.), and the contract test
value-semantic fragments follow the same wording. Both mono and multi
drive references also list per-policy name/description examples
(external_share, node_spread with NOBODY, node_move_forbidden) quoted
verbatim from the server i18n single source.
Sync the get_permission_setting result contract with the finalized
server-side enums: switch values ON/OFF become ENABLED/DISABLED and the
node_spread_scope binary ALL_CONTENT becomes ALL_NODES (restriction
applies to all nodes vs previewable-only nodes), while policy codes,
threshold domains, permissionMode and shareScope stay unchanged.
Policies now carry per-entry name and description fields (Chinese label
and value semantics from the server i18n single source) as deterministic
required members, mirrored in the contract test assertions and both mono
and multi drive skill quick references.
Declare the drive.permission get_setting ResultSpec data schema from the
server-side output metadata so the field-level contract (permissionMode,
shareScope, policy codes and their typed value domains, inherited /
allowedValues semantics) is reviewed code guarded by contract tests, and
add field-quick-reference rows for the get-setting section in both mono
and multi drive skill references.
Auto-CR flagged that the destructive-command tests only checked the error
string, so a regression that let a command slip past the confirmation gate
(or dispatch the wrong payload) would go unnoticed.
Every user_required leaf across college-contact (15), edu-contact (2),
edu-group (1) and edu-app (3) is now verified in pairs with a fresh
non-dry-run capture caller: without explicit confirmation the gate must
return confirmation_required AND the caller must see zero calls; with
--yes the command must produce exactly one call carrying the correct
productID, tool name and complete argument payload.
Remove custom collegeContactConfirmDestructive function and rely on the
standard framework Confirmation: "user_required" in LeafSpec. This ensures
destructive commands return the proper category:validation/code:3 error
instead of category:internal/code:5.
Also adds run_all_edu_commands_test.go covering all 153 leaf commands
across 5 edu/college products.
The command matrix test with 5 additional edu products (153 leaf commands)
exceeds the 10-minute go test timeout on Windows CI runners. The dedicated
TestCrossPlatformCoverage* tests in individual edu test files already
provide 100% changed-code coverage without the matrix.
The CI platform coverage gate (macOS/Windows) only executes tests
matching ^(TestAllShortcuts|TestCrossPlatformCoverage). The edu/college
test functions used standard names and were not exercised during the
platform coverage run, causing 73.89% changed-code coverage (target 100%).
Changes:
- Rename all edu/college test functions with TestCrossPlatformCoverage prefix
- Add edu products to command_matrix_test.go selected map
- Add TestCrossPlatformCoverageOpenSupplementServersIncludesEduEndpoints
in pkg/edition to cover the new supplement server entries
Local verification: coverage-gate-platform reports 100.0000% (3336 stmts).
The schema catalog validator requires Examples to demonstrate actual
execution, not just --help. Replace all 65 occurrences with realistic
parameter examples derived from each command's required flags.
- schemaPublishedShortcutCount: 461→468 after merging aisearch/contact/live
shortcuts from upstream/main
- Fix P2: datasource-update --field-ids doc says "不传时同步全部字段" but
actual behavior keeps existing field config; fixed in usage guide and
reference
Rebuild the exact-entry reviewedCompatibilityExceptions carve-out in the
base-owned schema-compat checker for the three destructive batch-remove
tools whose confirmation PR #1085 tightens from not_required to
user_required (doc/doc.remove_permission, drive/drive.permission_remove,
wiki/wiki.remove_member). Because the compatibility gate builds its
checker from the PR merge-base, this carve-out has to land on main before
PR #1085 can pass; the entry set is exact (tool + field + old -> new), so
any other confirmation drift, including weakening a reviewed tool back to
not_required, still fails.
Address the P1 review finding on PR #1085: --members lets one call remove
up to 30 USER/DEPT/CONVERSATION/TAG members, where departments, chats,
and role groups can indirectly affect many more users, yet the remove
branches called the MCP tool right after argument parsing with Safety
confirmation=not_required.
- drive permission remove, doc permission remove, and wiki member remove
now declare confirmation=user_required. DeclareLeafMetadata installs
the ConfirmSafety gate automatically (deferred to the first
deps.Caller.CallTool so flag validation still fails first), so an
unconfirmed invocation exits with the typed confirmation_required
error and performs zero MCP calls; --yes, an interactive yes, or
--dry-run previews remain the supported paths.
- Pass framework confirmation errors through WrapErrorWithOperation
verbatim (new apperrors.IsConfirmationRequired). Text classification
misrouted them: command paths containing "permission" (drive/doc
permission remove) were re-reported as AUTH_PERMISSION_DENIED while
other paths (wiki member remove) lost their reason and degraded to
UNCLASSIFIED.
- Tests: TestPermissionMemberRemoveRequiresConfirmationBeforeToolCall
covers all three entry points for both --members and legacy --users —
unconfirmed rejects with zero MCP calls, --yes dispatches exactly one
call with the complete precise arguments, --dry-run previews without
calls. Existing remove tests inject root --yes for the assembly
assertions; blank --users still fails validation before confirmation.
Address the P2 review finding on PR #1085: NO_PERMISSION is a generic
code name also returned by non-document tools — attendance
get-self-setting (bossAttendStatNotify) and event-subscription attempts
have both been observed returning it — so keying drive permission
apply-* guidance on it would mislead those products, defeating the goal
of the P1 scoping fix. Only the drive-specific forbidden.* domain codes
(forbidden.no.auth / forbidden.accessDenied) and the role-threshold
message wording remain document signals; a bare NO_PERMISSION still
classifies as AUTH_PERMISSION_DENIED but now keeps the product-neutral
suggestion, and NO_PERMISSION combined with document wording still gets
apply guidance.
Add regression tests for the non-document NO_PERMISSION case and update
the changelog fragment; changed-code coverage stays at 100%.
Address the two P1 review findings on PR #1085:
- Permission suggestions: the drive permission apply-* guidance is now
limited to document/wiki-specific errors (node access codes
NO_PERMISSION / forbidden.no.auth / forbidden.accessDenied and the
role-threshold wording). Permission failures from other products keep
their product-specific suggestion (e.g. the mail mailbox hint) or fall
back to a product-neutral hint instead of being told to run document
permission commands that cannot fix their problem.
- Null rendering: the null->{} adaptation is limited to the four tools
with a confirmed empty-response-means-success contract
(update_permission / remove_permission / update_member /
remove_member). Every other tool keeps its raw null output so the
shared machine-output contract stays unchanged.
Update tests and the changelog fragment accordingly; changed-code
coverage stays at 100%.
The server rejects the legacy maxResults path; the CLI now validates
--limit (1-50) and sends it as pageSize at runtime. Schema-compat
rejects a non-empty property redirect (maxResults -> pageSize), so
declare --limit as a CLI pagination input via the reviewed mapping
exclusion ledger (property omitted, provenance
reviewed_mapping_exclusion) on doc.list_permission,
drive.list_permission, and wiki.list_member.
- --notify now defaults to false and is omitted from the server request
unless passed explicitly (help updated accordingly)
- forbidden.accessDenied / permission-denied bodies classify as
AUTH_PERMISSION_DENIED with apply-permission guidance
- user/member validation failures intercepted before RESOURCE_NOT_FOUND
with --members corpId suggestion
- business error display appends backend code/logId for traceability;
literal null tool responses render as {}
- drive/doc permission list + wiki member list declare cursor pagination
(next-token) in Contract; cobra.NoArgs hardening on permission leaves
- cross-platform coverage tests and release fragments updated
- Add trimNonEmpty for --table-ids in +datasource-sync and --task-ids in
+datasource-sync-status, matching the existing field-ids pattern
- Add 4 test cases: whitespace-only rejection and trim-through for both
- Fix usage guide: typical workflow and notes no longer equate result
with processCode; correctly describe result as JSON to parse for
approvals[].processCode/name/iconUrl/url
--field-ids is declared as FlagStringSlice, but DatasourceCreate and
DatasourceUpdate previously called rt.Str to check whether the flag
was empty. RuntimeContext.Str delegates to cobra's GetString, which
returns an empty string on slice-typed flags, so the empty-value
guard rejected every explicit --field-ids input and the downstream
MCP tool never received fieldIds.
Switch to rt.StrSlice, sanitize through a new trimNonEmpty helper
(drop whitespace-only / empty entries) and pass the cleaned slice
to MCP. Add success-passthrough tests for both create and update,
plus a whitespace-only rejection case, and enhance the mock caller
to record MCP arguments so fieldIds can be asserted.
- Update --field-ids description in create/update shortcuts and the
helper-layer datasource update to clarify that omitting the flag
keeps existing config (create defaults to all fields), matching the
actual update overwrite semantics.
- Rename datasource shortcut coverage tests to the
TestCrossPlatformCoverage* prefix so they are picked up by the
macOS platform coverage gate.
Align shortcut layer validation with helper layer to prevent empty slices
from being sent to MCP, which could clear sync field selection due to
datasource update's overwrite semantics.
- Add empty string checks for --field-ids in both create and update shortcuts
- Add empty string checks for --auto-sync-setting in both create and update shortcuts
- Add regression tests verifying MCP is not called when empty values are rejected
- Both public entry points now have consistent validation behavior
Fixes P1 auto-CR issue for empty flag bypass vulnerability.
Make +datasource-sync-status consistent across shortcut and native
commands: --task-ids is now required, descriptions focus on querying
by taskId, and optional/IDLE semantics are removed. Update tests,
usage guide, reference doc, and SKILL description accordingly.
Native datasource create/update now expose --field-ids and
--auto-sync-setting, matching the shortcut-layer capabilities:
- flags registered on both commands
- Contract Parameters updated
- values mapped to MCP tool args
- JSON validation for --auto-sync-setting
- no-change update guard now counts the new flags
Also fixes the missing required name in the usage-guide update example.
+datasource-update now requires at least one mutable option
(--source-config, --auto, --field-ids, or --auto-sync-setting)
before calling update_datasource_config, preventing accidental
sync triggers. The native datasource update command enforces the
same guard for its supported flags. Also adds the required name
field to the +datasource-get-fields doc example.
Omitting --auto on +datasource-update previously sent auto=false to
MCP, silently disabling auto-sync for existing datasources. Now auto
is only included in tool args when the flag is explicitly provided,
so --auto=true and --auto=false work while omission preserves the
existing setting. Updated flag descriptions and added tests.
MCP requires the auto field in create_datasource / update_datasource_config
requests. Previously CLI only sent it when --auto was explicitly changed,
causing failures when users omitted the flag. Now both shortcut and helper
layers always include auto=false by default.
Also update flag descriptions and docs to clarify that the field is always
sent downstream, and add test assertions for the default-false behavior.
- docs/datasource-usage-guide.md: clarify that list-sources result is a
JSON string containing approvals[]; add missing --auto-sync-setting
parameter table rows and a dedicated autoSyncSetting format section
using the correct scheduled/daily/weekly/monthly enums.
- skills/references/aitable/aitable-datasource.md: fix autoSyncSetting
enums (schedule/day/week/month -> scheduled/daily/weekly/monthly) and
update the create example accordingly.
The OA approval source-config contract requires processCode, name,
iconUrl, and url to be passed through unchanged from +datasource-list-sources.
Published examples for +datasource-create, +datasource-update, and
+datasource-get-fields were missing `name`, and the usage guide marked it
as optional. Fix all examples in the shortcut layer, helper layer, and
docs; update flag descriptions to mention name; and add a contract test
that validates every delivered example's source-config JSON contains the
required members.
Both the shortcut (+datasource-sync, +datasource-sync-status) and
helper (datasource sync, datasource sync-status) layers now validate
that table-ids contains 1-5 IDs and task-ids contains at most 5 IDs
before calling MCP, matching the declared contract.
ValidateRequiredFlags calls GetString which returns empty for
StringSlice flags, causing the examples test to report --table-ids
as missing. Switch to String + parseCSVValues to match the codebase
convention used by record-ids and other comma-separated flags.
- Add 7 datasource leaf commands to internal/helpers/aitable.go so
coverage test can find tool name literals (fixes TestAllShortcutsAssemble)
- Add 7 entries to semantic_catalog_aitable.json and update catalog count
from 93 to 100 (fixes TestCrossPlatformCoverageAITableSemanticCatalog)
- Update publicShortcutCount/schemaPublishedShortcutCount/publiclyDelivered
from 422/447/422 to 429/454/429 (fixes TestDeliverySchemaCoversOrExactly)
- Fix Contract.Selection.AgentSummary and UseWhen[0] in datasource.go to
match Description and Intent exactly as required by schema contract test
- Fix autoSyncSetting enum: scheduled/daily/weekly/monthly; mark
selectedMonthDays/selectedWeekdays as required for monthly/weekly
- Remove splitParentTableField from --source-config user-settable fields;
add note that splitParentTableField/enableDataSyncOaDetailList are
internal downstream fields not to be passed
- Prepend sync-is-fire-and-forget notice to DatasourceSync descriptions
- Remove --conflict-strategy flag (syncConflictStrategy not in MCP schema)
- Add --auto-sync-setting flag to DatasourceCreate (was only in Execute, not in Flags)
- Expand DatasourceSync description: add 文档链接, errorCode=4014 幂等冲突, 非数据源表参数错误
- Simplify DatasourceGetFields description: remove field property enumeration to match snapshot
- Add --auto-sync-setting flag (JSON string) to +datasource-create and
+datasource-update, validated and passed through as raw string.
- Update +datasource-update --source-config desc to reflect full
replacement semantics ("传入时整体覆盖") and spell out required /
optional fields with defaults.
- Append "仅支持 OA 审批数据源 (datasourceType=OA)" to
+datasource-get-config description.
- Simplify +datasource-list-sources / +datasource-get-fields
descriptions to concise Chinese aligned with snapshot wording.
- Update SKILL.md shortcuts table and add datasource usage guide.
Add 5 data source sync management shortcuts to the aitable service:
- +datasource-create (create_datasource): create sync config + first sync
- +datasource-update (update_datasource_config): update existing sync config
- +datasource-sync (run_datasource_sync): trigger manual sync (max 5 tables)
- +datasource-sync-status (get_datasource_sync_status): query sync task status
- +datasource-get-config (get_datasource_config): get sync config details
Each shortcut declares a full Contract (Identity/Interface/Selection),
Safety, Flags, and Execute that calls rt.CallMCPData on the "aitable"
MCP server. datasource-type is passed through without CLI enum check;
source-config is validated as a JSON object via parseJSONObject.
Sync the permission CRUD overhaul from the internal CLI (MR 28965577):
- drive/doc permission list and wiki member list now accept --next-token
to follow the server cursor (totalCount/hasMore/nextToken); --limit maps
to pageSize capped at 50 instead of the rejected maxResults=200 path
(fixes#1065)
- permission add/update/remove and wiki member add/update/remove accept a
--members JSON array (USER/DEPT/CONVERSATION/TAG grantee types, each with
its own roleId) with optional --notify; legacy --users/--role stays
- cursor/page-token hidden cross-product aliases now resolve to next-token
- regenerate param_aliases_generated.go; wiki member list override no
longer blocks cursor
- update mono/multi skill references and add change fragment
A stamp-shaped directory name is not ownership proof: pruneSkillBackups
counted and RemoveAll'd any 20260819-120000-shaped entry under
~/.dws/skill-backups, so a user or tool that created such a directory
lost its contents once DWS held five backups, and the Go backup path
(MkdirAll) adopted a same-named foreign root outright. The PowerShell
installers already implemented the correct contract; every other
surface now matches it.
Go stamps a freshly created root with the exact marker bytes the
install scripts write (.dws-skill-backup = "dws skill backup v1")
before any payload moves in, claims the root with mkdir so an existing
unproven root bumps to a collision suffix instead of being adopted,
and prunes only roots whose marker verifies — unmarked or wrongly
worded stamp-shaped directories are foreign data, preserved and never
counted against the keep limit. The shell installers (install.sh,
install-skills.sh, install-event.sh, install-devapp.sh) and the npm
installer apply the same rule in their backup collision loops, with
roots recorded as created by the running process exempt from marker
re-verification so a mid-run marker permission failure still reuses
this run's own root and keeps the sibling payload intact.
Regression tests cover every surface: pruning an unmarked/wrongly
marked stamp-shaped directory alongside marked ones, refusing to adopt
a foreign root (payload moves to a suffixed root, foreign data and its
nonexistent marker untouched), same-stamp reuse of a proven root, and
marker-write failure cleaning the empty fresh root.
copy_tree published staged children with mv, which replaces a
concurrently created same-name directory (POSIX rename succeeds over an
empty target) and whose rollback moved every dest child back — including
a concurrent writer's different-named entries — before deleting the
staging tree. Children now publish through kernel-level no-clobber
primitives (mkdir claim + recursion for directories with the recorded
mode restored, ln for regular files, ln -s for symlinks), a manifest
records exactly what this transaction published, the rollback retracts
only those entries in reverse order, and each level re-counts the
destination so a foreign different-named entry aborts the publish with
the destination retained. Read-only staged directories (0555 skill
trees) are made owner-writable for the move; the backup restore uses the
same discipline so a concurrent writer is refused without partially
draining the backup.
Regression tests cover both scripts: a concurrently created same-name
empty child directory and a different-named foreign entry mid-publish
are retained with the original backup kept; both fail against the
previous mv-based implementation.
The platform coverage gates execute only TestCrossPlatformCoverage-named
tests, so the child-move error and dispatch branches that the full local
suite covered incidentally were reported as uncovered changed code on
Windows (96.78% vs the 100% target). Adds a seam-driven edge suite for
the child-move fallback — source/claim/child stat and read failures,
per-child link and symlink collisions and publish failures, rollback
rename failure, foreign-entry abort, mode-restore failure, source shell
removal failure, nested-directory and simulated-symlink children, and
post-rename content drift — plus the retained-destination notice for a
dependent uncertain target in skill setup. The POSIX file identity impl
now consults the lstat seam so its degradation branches are coverable
the same way. Verified against the gate's own changed-line computation:
zero uncovered changed statements in internal/upgrade.
The publish-confirmation and tunneled-replacement tests physically
removed and reseeded the destination to simulate a concurrent swap. On
NTFS the recreation can immediately reuse the freed MFT record, making
the file ID (volume serial + file index) compare equal and the proof
pass against a replaced object — the Windows coverage gate observed the
confirmation falling through to the fingerprint branch instead of the
identity branch. Both tests now force the replacement through the two
primitives the platform proof consults (os.SameFile on Unix, the file-ID
seam on Windows), matching the technique the tunneled-rollback case
already used for Unix inode recycling.
skill_publication_identity_linux_test.go pinned the remote line's
Statx/birth-time identity design (skillPathStatx seam); the merged head
proves ownership with dev:ino plus the fingerprint backstop instead, so
the test no longer compiles on Linux. Caught by CI's Linux lint job,
which builds what macOS-local vet skips behind the linux build tag.
- Reject --request payloads with a missing, empty, or non-string
processCode; the backend answers a bad processCode with success:true
and an empty list, so validate client-side like startTime
- Add regression cases to keep changed-code coverage at 100%
Reconciles the two parallel evolutions of PR #996 with this session's
publication design as authoritative:
- internal/upgrade, internal/app: ours — mkdir-claim identity witness
(dev:ino on POSIX, volume file ID on Windows), three-state ownership,
ErrSkillPathPublicationUncertain, copy-fallback short-circuits. Drops
the remote line's xattr publication-mark design and its six follow-up
fixes (retract contracts, Statx token); skill_publication_mark_*.go
removed accordingly.
- scripts/, build/npm/, test/scripts/, docs/rfc: theirs — same replayed
install hardening plus main's evolution and the npm no-clobber child
moves; no xattr dependency, consistent with the claim model.
- .changes: their npm/Shell/PowerShell narrative with the Go-design
sentences rewritten for the uncertain-publication contract.
Verified: go build, go vet (tests compiled), gofmt, and package tests
for internal/upgrade, internal/app, test/scripts all green on this tree.
The mkdir->rename->remove->rename directory fallback had a TOCTOU window
between the second remove and the second rename: a concurrent writer
creating an entry at the destination was silently clobbered. The fallback
now claims the destination once with mkdir and never unlinks it: the
fast-path rename publishes over the claim (Linux), and platforms that
refuse directory renames (macOS, Windows) move the staged children into
the claim through atomic no-clobber primitives (mkdir/os.Link/os.Symlink),
consuming the emptied source shell on success.
renameSkillPathNoReplace now returns the mkdir-claim identity captured by
the child-move path. PublishSkillPathNoReplace uses it as a three-state
ownership witness: the atomic/fast paths keep the staged-inode proof, the
child-move path proves dest is still the mkdir claim, and a mismatch
reports the new ErrSkillPathPublicationUncertain sentinel with the
destination retained. The witness is real on POSIX now: darwin and linux
report the dev:ino file identity instead of the empty no-op.
Upstream consumers honor the sentinel: the mono/multi upgrade copy
fallbacks no longer retry over an uncertain destination (the retry would
displace the concurrent writer's object), and skill setup reports the
retained destination instead of claiming a rollback.
Rewrites the fallback tests that pinned the removed remove-and-retry flow
and adds regression coverage: concurrent claim entries abort with the
destination retained, wholesale replacement after child-move reports the
uncertain sentinel, staged-set transactions pass the sentinel through,
and both copy fallbacks short-circuit (ablation-verified).
- Reject --request payloads missing startTime (documented required) so
endTime can no longer bypass validation when startTime is absent
- Align --request time ordering with simple mode: endTime must be
strictly after startTime
- Cover all five previously uncovered branches (pageSize absent,
startTime absent, malformed endTime, valid time pair, empty --start
flag) to reach 100% changed-code coverage
- Add dws oa approval list-by-admin leaf with simple flags and
advanced --request modes backed by get_process_instances_by_admin
- Send startTime/endTime as yyyy-MM-dd HH:mm:ss strings per the
2026-08 MCP contract update; ISO-8601 flag inputs auto-convert
- Enforce pageSize cap (20) and string time format/order client-side;
PreRunE reports flag-group violations in Chinese before Cobra's
built-in English validation
- Extend coverage tests and document the command in mono/multi OA
skill references
Windows coverage gate only runs TestCrossPlatformCoverage*, and the
xattr mark helpers are Unix-only. Inject seams so marked dest is
retracted on owned drift, left in place when the mark is gone, and
the helper error paths are exercised on every platform.
Linux overlayfs recycles device+inode, so SameFile and a lone inode
token treated a replacement as owned and retracted it. Stamp staged
inodes with an xattr mark, prove Linux/Darwin identity with birth
time, and make shell copied-set rollback check dest first with inode
plus child names.
Match the Go dest-first identity check so a concurrent replacement is
never moved into .rollback-*; only a post-quarantine mismatch is
restored with no-replace. Cover both races in the npm smoke suite.
Record dest on occupy and retract it when confirmation, verify, or
staging cleanup fails. Restore unmatched quarantine with a no-replace
publish. Event/devapp copy uses mkdir-claim; shell rollback claims dest
before delete. Release copy now says npm/PowerShell create junctions and
Go uses os.Symlink, with copy fallback when linking is unavailable.
Cross-filesystem Skill moves now record publication identity as soon
as the staging path is renamed onto dest. A later mode-restore, copy
verification, or staging-cleanup failure retracts that proven dest so
retries are not blocked by an untracked leftover. A failed retract
reports an uncertain state naming both retained locations.
The shell mono/multi set publishers staged each Skill directory and
published it with a plain mv after the backup; anything another process
created at the destination between the backup and the move was silently
replaced, and restore_multi_skill_set then blind-deleted manifest paths,
so a concurrently replaced object could also be destroyed during
rollback. Publish through an atomic mkdir claim instead — EEXIST refuses
any occupant, staged children move into the claim one by one, and a
failed child move relocates them and removes only the claim. The
published manifest now records <dest>:<inode>, and rollback deletes a
destination only when its inode still matches the publication, skipping
concurrently replaced paths with a warning. Also fixes a latent
unbound-variable expansion where a shell variable was followed directly
by a full-width parenthesis in a message. Regression tests publish a
first Skill, replace it with a foreign directory, fail the second
publication, and assert rollback retains the foreign object untouched
while restoring the rest from backups.
The RFC section on filesystems that reject the atomic no-replace rename
still described the retired existence-check-plus-plain-rename fallback
and its accepted race window. The implementation (and the npm and shell
surfaces) claim the destination with mkdir or a hard link — or create
the link directly at the destination — and never release the claim mid
transaction, so a concurrently created object is refused rather than
overwritten. Record that contract and its only relaxed property (child
moves are not all-or-nothing visible) so future maintainers do not
port the racy description back into code.
- add success and failure outcomes for three attachment commands
- define business data schemas and mark downloadUri as sensitive
- migrate attachment commands to unified result output
- verify compact and full Schema result projections
- cover success, malformed response, and tool error paths
to #666
The POSIX shell installers staged shared Skill links and published them
with mv after an existence check; a file or symlink another process
created at the destination between the check and the move was silently
replaced, and the inode confirmation could not detect the loss. Publish
by creating each link directly at its destination instead — symlink(2)
refuses an occupied path with EEXIST, so the creation itself is the
atomic no-replace check. A directory that appears at the destination
turns ln -s into a container; the nested link is removed after an
identity check and the transaction rolls back, leaving the foreign
directory untouched. Applied to install.sh, install-skills.sh,
install-event.sh, and install-devapp.sh. Also covers the remaining
retraction branches of the Go shell-removal fallback so changed-code
coverage is complete. Regression tests inject a concurrent occupant at
the publish instant for regular-file and directory cases and assert the
foreign object and its contents stay completely unchanged.
On filesystems without atomic no-replace rename, the degraded
publication moves the source children into a fresh claim and leaves an
emptied source shell for the caller to remove once the move is
confirmed. If that removal failed, moveSkillPathRecoverably reported a
plain failure claiming both locations were preserved while the data
existed only at the destination, so backupAndRemoveSkillDir never
recorded the backup and the original path was left empty. Move the
children back into the shell and withdraw the destination instead; a
failed retraction reports the data location explicitly. Restores the
contract that a failed move keeps the source intact.
publishCanonicalLinkNoReplace checked the destination with lstat and
then published, leaving a window the comment claimed did not exist: on
Windows renameSync replaces a concurrent object outright (libuv passes
MOVEFILE_REPLACE_EXISTING), and on POSIX ln -P source target links INTO
a directory that appeared at the target, leaving a stray link inside
foreign data that the rollback list never recorded. Create the symlink
or junction directly at the destination instead — link creation fails
with EEXIST when anything occupies the path and never treats the target
as a container, so the publication itself is the atomic no-replace
check. Identity confirmation re-reads the live link before the
publication enters the rollback list. Covered by injected concurrent
creators at the publish instant on POSIX and simulated Windows,
asserting the foreign object and its contents stay completely
unchanged.
The mono and multi set copy publishers checked destination existence
with lstat and then called Node's rename, which replaces the target on
every platform (libuv passes MOVEFILE_REPLACE_EXISTING on Windows). A
file, symlink, or empty directory created between the check and the
rename was silently overwritten, and the identity confirmation could not
recover it because the publication record only proved the staged object
arrived. Claim the destination with mkdir — which fails with EEXIST if
anything occupies the path, so the claim itself is the existence check —
and move the staged children into the claim, restoring the source mode
on it. A failed child move relocates the children back and removes only
the claim. Covered for mono, multi, and simulated Windows, including an
injected concurrent creator at the claim instant.
The no-replace file fallback links the destination and then removes the
source. If the removal fails, the caller treats the publish as failed,
but no publication record exists to roll the new destination back, and
a backup restore would refuse the occupied path. Remove the destination
behind an identity check — only the proven linked object may be deleted
— and report when the retraction itself fails or the destination was
concurrently replaced.
The previous wrapper returned the first observed ID for both paths, so
expected == actual still held on Windows and the proof accepted the
swap. Return a distinct ID for the second probe.
The constant file-ID stub made both IDs equal, so the Windows proof
(expected == actual) accepted the publication and the subtest failed
there; Unix stayed green because its proof ignores the ID strings and
the swapped os.SameFile seam already forced the failure. Return the
first observed ID for both paths so staged and published identities
differ on every platform while real IDs still flow through the wrapper.
The physical same-content swap relied on the recreated destination
getting a fresh inode, but CI runners' ext4/overlayfs recycle inodes
eagerly, so the swap was undetectable on Linux and the subtest failed
there (while passing on macOS). Swap the same-file identity seam instead
so the confirmation's fast-path rejection contract is pinned on every
platform.
The four standalone installers pruned the oldest excess stamp
directories regardless of origin, so a migration retiring more than
five batches destroyed its own rollback material mid-run — the same
data loss already fixed for Go via the run-root registry and present
in install.js/install.ps1 as currentRunBackupRoots. Every installer now
records the stamp directories it creates and pruning only removes
earlier-run batches, which is what the changelog already promises.
The degraded directory publication claimed the destination with mkdir, then
— on platforms whose rename refuses to replace a directory (macOS refuses
even an empty target, verified empirically) — removed the claim and retried
a plain rename. Between the unlink and the retry a foreign directory could
appear at the destination and be silently overwritten, breaking the
no-replace contract the fallback exists to provide.
Hold the claim for the whole transaction instead: rename over the claim
where the platform permits it (Linux), otherwise move the source children
into the claim one by one. The destination is never unlinked, so a
concurrent creator can only ever lose the mkdir race; every child rename
targets a nonexistent path inside the empty claim, and a failed move
restores the children and removes only the claim.
The child move legitimately changes the publication's identity, which the
confirmation now handles: a rename that consumed the staged path is still
proven by identity, while a child move is proven by the pre-rename content
fingerprint. The emptied source shell doubles as the signal distinguishing
the two shapes; moveSkillPathRecoverably removes it to keep move semantics.
The backup stamp has second precision and pruning kept only the newest 5
stamps, so a canonical migration that retires copies across many Agent
roots deleted its own earlier backups mid-run. That silently voided the
reversibility guarantee the transaction depends on for rollback: a probe
retiring 8 paths lost 3 of them permanently.
Record every stamp directory this process creates, keyed by normalized
absolute path, and prune only the oldest foreign stamps.
Creating the staged symlink usually succeeds, so the link strategy really
fails at publish time: renameSkillPathNoReplace has no atomic no-clobber
primitive for a symlink source and refuses it whenever the kernel flag is
unavailable (NFS, FUSE, overlayfs). Gating the copy fallback on staging
alone therefore left every non-universal Agent unconfigured on exactly the
filesystems the fallback exists to support.
Retry the whole target transaction as a direct copy after a failure in any
phase, but only when the failed attempt fully restored the originals. The
converter also re-adds the replacement backups the link plan deliberately
skips for destinations already pointing at canonical, which a copy must
replace and no-replace publication would otherwise reject with EEXIST.
skillPathSameFileIdentityImpl on Windows always returns false and is
never reached through skillPathIdentityProven (which uses file IDs
exclusively). Add a direct seam call with synthetic os.FileInfo to
exercise the Windows return-false path and the Unix os.SameFile path
with nil Sys().
Restructure skillPathFileIdentityImpl to use nested if-err-nil with a
named return and skillPathIdentityProven to use a single expression.
Error conditions now fall through to the bare return instead of
occupying separate coverage blocks, eliminating 5 uncovered statements
that the Windows coverage gate flagged at 99.4193%.
Add FILE_FLAG_OPEN_REPARSE_POINT to the Windows CreateFile call in
skillPathFileIdentityImpl so symlinks are opened as reparse points
rather than followed to their target. Staged symlinks carry relative
targets computed for the final destination, which may not resolve from
the staging directory; following them caused CreateFile to fail,
yielding an empty file ID that rejected publication and broke canonical
skill layout migration on Windows.
Make skillPathSameFileIdentity a seam variable so the tunneled
replacement test can deterministically simulate the identity change on
Unix. On tmpfs (used by Linux CI runners), os.SameFile can return true
for a recreated file due to inode reuse, making the test flaky. On
Windows the swap is a no-op because skillPathIdentityProven compares
file IDs from GetFileInformationByHandle and ignores
skillPathSameFileIdentity.
NTFS file tunneling can restore the original creation time for a
recreated same-named object, which defeated the creation-time
incarnation check and allowed rollback to delete a concurrent
replacement. Replace the platform-specific identity pair with a single
skillPathIdentityProven function:
- Unix: delegates to os.SameFile (inode/dev), ignoring file ID strings
- Windows: compares VolumeSerialNumber:FileIndexHigh:FileIndexLow from
GetFileInformationByHandle, which uniquely identifies the file on the
volume for its lifetime and is unaffected by tunneling
When the file ID cannot be obtained at publish time, identity is not
proven and the auto-delete is refused. Add a regression test that
simulates tunneled creation time and verifies rollback still refuses
the concurrent replacement.
On Windows isNoReplaceRenameUnsupported always returns false, so the
fallback is never entered from the invalid-path test. Force the fallback
and swap skillPathLink to a non-EEXIST error to cover line 94 on all
platforms.
Add tests for mkdir non-EEXIST error, remove failure after rename
failure, first-rename-succeeds path (Linux behavior), retry-rename
path, and non-regular source safe-fail. All 24 changed executable
statements now covered on both macOS and Windows.
The fallback path for filesystems without RENAME_NOREPLACE/EXCL (NFS,
FUSE, overlayfs) used Lstat-then-Rename, which could overwrite a
concurrently created destination between the check and the rename.
Replace the TOCTOU-prone check with truly atomic no-clobber primitives:
- Directories: os.Mkdir atomically claims the destination (fails with
EEXIST if occupied). On Linux rename(2) replaces the empty dir
directly; on Darwin/Windows rename refuses existing dirs so the empty
dir is removed and the rename retried — any concurrent creation
between remove and rename is detected by the second rename failing.
- Files: os.Link atomically fails if the destination exists, then
os.Remove completes the move.
Add concurrent-creation test covering the mkdir→rename race window.
The !os.IsNotExist(statErr) branch in renameSkillPathNoReplace was
uncovered on Windows. Inject errNoReplaceRenameUnsupported for the
atomic rename and os.ErrPermission for skillPathLstat so the stat-error
path is exercised on every platform.
The smoke test creates temp home directories with .config/kimchi markers
for agent detection. On Linux CI runners XDG_CONFIG_HOME may point to the
runner's real config path, causing resolvedAgentTargets to look outside
the temp home. Unset it so detection resolves against the test's temp dir.
Restrict backup pruning to directories whose names match the DWS stamp
format (YYYYmmdd-HHMMSS with optional -N suffix) across all 8 installer
surfaces (Go, npm, 4 shell, 2 PowerShell). Unknown directories in
~/.dws/skill-backups are now preserved. Also fixes Windows coverage test
portability and covers the remaining macOS changed-code gap (retire
warning loop in runUpgrade).
A universal Agent whose obsolete private copy cannot be retired installs
nothing there, yet every entry point counted that retirement failure as an
install failure — aborting `npm install`, `dws skill setup`, and the shell
installers even when the canonical store and all links published correctly,
and skipping the skills-state write. Route retirement failures to a separate
warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell,
install.sh, install-skills.sh, install-event.sh, install-devapp.sh).
Also:
- Add a checked-rename fallback for filesystems that reject the atomic
no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and
the previously unsupported platforms build and work.
- PowerShell multi-mode links only bundle skills, never the shared canonical
store, so third-party/user skills are no longer fanned into every Agent root.
- Prune ~/.dws/skill-backups to the newest 5 on every surface; encode
HOME-relative backup names on PowerShell to preserve origin.
- Add simulated-win32 junction coverage and rewrite the tautological
no-replace test; remove dead code whose tests gave false coverage.
- Soften the overstated Windows ownership-proof comment (NTFS tunneling).
- extend the silent-rollback contract to install-event.sh
- static contract: Restore-MultiSkillSet removes published paths lexically
(section-scoped so identity-anchor refactors keep the guarantee) and link
staging dirs are cleaned via Remove-LinkStageRoot / Remove-DevLinkStageRoot
- install-event.sh integration test: an uninstallable agent target is
skipped loudly while later agents still receive links
- pwsh probe: Test-SamePhysicalSkillRoot must dereference junctions and
symlinks (junction idempotency asserted where junctions are creatable)
- install.ps1: remove published junctions lexically in Restore-MultiSkillSet
(Windows PowerShell 5.1 follows reparse points during Remove-Item -Recurse
and could delete canonical store contents); clean link staging dirs
lexically in Publish-CanonicalSkillLinks and Move-SkillPathRecoverably
- install.ps1: Test-SamePhysicalSkillRoot now dereferences junctions via
Get-PhysicalSkillPath (mirrors EvalSymlinks/realpathSync/cd -P), so reruns
recognize already-published junctions instead of backup churn
- install-event.sh: replace silent 'mv ... 2>/dev/null || true' rollback with
the loud backup-retained failure contract already enforced for devapp
- event/devapp sh+ps1: link→copy fallback and per-agent failures now degrade
per agent like install.sh (skip loudly, continue, report at the end)
instead of aborting mid-loop or swallowing errors
- tests: junction-lexical removal contract, event per-agent degrade
integration test, pwsh junction physical-root recognition + rerun
idempotency (no backup churn)
A failed canonical publish only failed the upgrade when
hasDependentSkillRoot reported a non-universal link target; that helper
explicitly skipped universal agents, which are exactly the direct consumers
of ~/.agents/skills. On a universal-only machine (e.g. only Codex
installed), UpgradeSkillLocations* returned a nil error with nothing
installed, contradicting the documented "canonical publication is
mandatory and fails the upgrade loudly" contract.
Canonical publish failures now return an error unconditionally in both the
mono and multi branches, and hasDependentSkillRoot is removed. The test
that pinned the old standalone-does-not-fail-fast behavior now asserts
error propagation in both modes.
The allowSystemApps gate (homeDir == systemHome) was effectively a no-op in
production: systemHome came from os.UserHomeDir, which honors the $HOME env
override just like homeDir, so the two were always equal and the gate never
fired when $HOME was overridden.
ResolveSystemHomeDir now prefers the OS user database (getpwuid on Unix),
which is independent of $HOME, falling back to $HOME only when the user record
cannot be resolved. Production behavior is unchanged (a real $HOME still
matches); an isolated/overridden HOME now correctly skips machine-wide
/Applications discovery for zcode/minimax. The app surface references the same
shared resolver.
This is the correct fix for the hermeticity concern (machine-wide state leaking
into an isolated HOME): there is no cross-surface production inconsistency to
port — script installers always operate on the real user HOME in practice, so
they need no gate.
skillPathSameFileIdentityImpl on Windows always returns false and is
never reached through skillPathIdentityProven (which uses file IDs
exclusively). Add a direct seam call with synthetic os.FileInfo to
exercise the Windows return-false path and the Unix os.SameFile path
with nil Sys().
Restructure skillPathFileIdentityImpl to use nested if-err-nil with a
named return and skillPathIdentityProven to use a single expression.
Error conditions now fall through to the bare return instead of
occupying separate coverage blocks, eliminating 5 uncovered statements
that the Windows coverage gate flagged at 99.4193%.
Add FILE_FLAG_OPEN_REPARSE_POINT to the Windows CreateFile call in
skillPathFileIdentityImpl so symlinks are opened as reparse points
rather than followed to their target. Staged symlinks carry relative
targets computed for the final destination, which may not resolve from
the staging directory; following them caused CreateFile to fail,
yielding an empty file ID that rejected publication and broke canonical
skill layout migration on Windows.
Make skillPathSameFileIdentity a seam variable so the tunneled
replacement test can deterministically simulate the identity change on
Unix. On tmpfs (used by Linux CI runners), os.SameFile can return true
for a recreated file due to inode reuse, making the test flaky. On
Windows the swap is a no-op because skillPathIdentityProven compares
file IDs from GetFileInformationByHandle and ignores
skillPathSameFileIdentity.
NTFS file tunneling can restore the original creation time for a
recreated same-named object, which defeated the creation-time
incarnation check and allowed rollback to delete a concurrent
replacement. Replace the platform-specific identity pair with a single
skillPathIdentityProven function:
- Unix: delegates to os.SameFile (inode/dev), ignoring file ID strings
- Windows: compares VolumeSerialNumber:FileIndexHigh:FileIndexLow from
GetFileInformationByHandle, which uniquely identifies the file on the
volume for its lifetime and is unaffected by tunneling
When the file ID cannot be obtained at publish time, identity is not
proven and the auto-delete is refused. Add a regression test that
simulates tunneled creation time and verifies rollback still refuses
the concurrent replacement.
On Windows isNoReplaceRenameUnsupported always returns false, so the
fallback is never entered from the invalid-path test. Force the fallback
and swap skillPathLink to a non-EEXIST error to cover line 94 on all
platforms.
Add tests for mkdir non-EEXIST error, remove failure after rename
failure, first-rename-succeeds path (Linux behavior), retry-rename
path, and non-regular source safe-fail. All 24 changed executable
statements now covered on both macOS and Windows.
The fallback path for filesystems without RENAME_NOREPLACE/EXCL (NFS,
FUSE, overlayfs) used Lstat-then-Rename, which could overwrite a
concurrently created destination between the check and the rename.
Replace the TOCTOU-prone check with truly atomic no-clobber primitives:
- Directories: os.Mkdir atomically claims the destination (fails with
EEXIST if occupied). On Linux rename(2) replaces the empty dir
directly; on Darwin/Windows rename refuses existing dirs so the empty
dir is removed and the rename retried — any concurrent creation
between remove and rename is detected by the second rename failing.
- Files: os.Link atomically fails if the destination exists, then
os.Remove completes the move.
Add concurrent-creation test covering the mkdir→rename race window.
The !os.IsNotExist(statErr) branch in renameSkillPathNoReplace was
uncovered on Windows. Inject errNoReplaceRenameUnsupported for the
atomic rename and os.ErrPermission for skillPathLstat so the stat-error
path is exercised on every platform.
The smoke test creates temp home directories with .config/kimchi markers
for agent detection. On Linux CI runners XDG_CONFIG_HOME may point to the
runner's real config path, causing resolvedAgentTargets to look outside
the temp home. Unset it so detection resolves against the test's temp dir.
Restrict backup pruning to directories whose names match the DWS stamp
format (YYYYmmdd-HHMMSS with optional -N suffix) across all 8 installer
surfaces (Go, npm, 4 shell, 2 PowerShell). Unknown directories in
~/.dws/skill-backups are now preserved. Also fixes Windows coverage test
portability and covers the remaining macOS changed-code gap (retire
warning loop in runUpgrade).
typedJSONValue marshaled a typed value and then routed the result through
rawJSONValue, which runs json.Valid before decoding. On that path the input is
whatever json.Marshal has just produced, so the validation scan can only ever
succeed: it re-read every marshaled document for nothing.
The decode step is now shared by both entry points. rawJSONValue keeps its
json.Valid check, because it still accepts untrusted input, while typedJSONValue
decodes what it marshaled directly. Across the 1121-tool set this removes about a
third of the Schema Catalog projection work: the internal/app schema suite goes
from 26.0s to 17.2s uninstrumented, and from 291.1s to 241.0s under -race.
The delivered Catalog is byte-for-byte unchanged. check-generated-drift,
check-schema-catalog and check-schema-binary each regenerate the same
source_hash sha256:93b8d44eb163bd2898c78397d22af92d378e3dc4e20f56b33277b51e4342e2e6,
and the two error contracts are preserved: typedJSONValue still rejects a value
json.Marshal cannot encode, and rawJSONValue still rejects invalid JSON.
The five internal/app partitions ran end to end inside one job, so the app
shard's wall clock was the sum of all five: 780s in CI, of which the schema
partition owned 357s. Each partition is now its own matrix shard, so they run
concurrently and the shard's wall clock is set by its slowest partition rather
than by their total. Every partition shard still selects the same single
internal/app package, so the impacted-package query maps the shard name back to
app and the partition only chooses which tests run.
The helper gains a partition argument and a list-partitions mode. APP_PARTITIONS
is the single source of truth for the set, and the discovery pass still runs in
every job, so each one independently verifies that the partition patterns cover
every top-level test exactly once before running the one it was asked for.
Two fail-closed checks guard the split, because the helper's own coverage check
can no longer prove the whole package ran once the partitions are separate jobs:
- The helper cross-checks APP_PARTITIONS against the coverage counters in both
directions, so a counted partition that nothing dispatches and a dispatchable
partition with no counter both fail instead of silently skipping tests.
- TestCIAppRacePartitionMatrixMatchesHelper pins the workflow's app-<partition>
shards to list-partitions output in both directions, so a partition cannot
lose its job while every job stays green.
The discovery loop variable is renamed from partition to spec: it would
otherwise shadow the partition requested on the command line, which run mode
reads after the discovery pass completes.
The schema partition's 52 tests assert structural Schema-to-Cobra contracts over
a single goroutine: none of them call t.Parallel or start a goroutine, so the
race detector has no concurrent access to observe there. The process-global lazy
metadata that does need race coverage (schema_source_root's atomic.Value, the
parameter-binding lazy loaders) is exercised by internal/cli's concurrent tests,
which stay instrumented.
The instrumentation was not free here. The partition shares a single sync.Once
Catalog build whose work is allocation-heavy, and -race made it roughly 11x
slower: 26s -> 291s locally, and 357s of the app shard's 780s in CI. Within that
partition TestFinalSchemaToolsHaveExecutableBaseCommands alone accounted for
262s, not because the test is expensive but because it is the first caller to pay
for the shared snapshot; its 1121 subtests together measure 0.00s.
run_partition now takes the instrumentation mode explicitly and fails closed on
an unrecognized value, so a typo cannot silently drop -race from a partition that
is supposed to carry it.
The workflow contract pinned the focused path by literal: the job name
`Test (changed packages)`, the unsharded
`list "$TEST_BASE_REF" "$TEST_HEAD_REF"` call, and a single
`go test -timeout=15m` line standing in for internal/app's package-level
headroom. Sharding the job changed all three literals, so `Test (workflow
and release contracts)` failed on this branch even though every shard
selection test passed.
Each invariant the contract guarded still holds, so the assertions are
updated to the new shape rather than relaxed:
- the focused job must still exist, now as the matrix job, named the way
the contract already names `Test (race: ${{ matrix.shard }})`;
- package selection must still derive from the authoritative synthetic
merge base/head, now with an explicit shard argument, so pointing it at
any other ref still fails the contract;
- internal/app's headroom is asserted through the process-isolating
helper and the per-shard budgets, mirroring the assertions already
applied to test-race. That is stronger than the old single -timeout: it
pins the mechanism that keeps the suite inside its budget rather than
the number alone. release-scripts membership is asserted too, because
its dedicated job only runs at full-suite or release-sensitive scope,
so losing it here would silently stop testing test/scripts changes.
The shard comparisons in the focused job are quoted so that job reads
verbatim like test-race's.
Ablating the implementation one change at a time turns the contract red
in all five cases: removing the app helper call, dropping release-scripts
from the matrix, selecting from HEAD~1, collapsing the matrix back to a
single unsharded job, and dropping the cli/smoke timeout budget.
Reading the package list with `mapfile < file` has unambiguous line
semantics. Routing it through a step output and a here-string instead
would append an extra empty array element if the value ever carried a
trailing newline, and that element would reach go test as an empty
package argument. The step output now carries only a single-line boolean,
and the list travels through RUNNER_TEMP. An explicit empty-entry guard
fails closed if the file is ever malformed.
This job cannot execute on its own pull request — editing a workflow
routes the revision to full_suite, which skips the focused path — so the
implementation deliberately avoids depending on platform-specific
trailing-newline behavior that local verification cannot observe.
The focused path tested every impacted package in a single job with a
plain `go test -race`, so internal/app ran inside one long-lived process
alongside all of its reverse dependencies. That is exactly the shape
scripts/ci/run-app-race-tests.sh exists to avoid: a single app test
process retains every constructed command tree in framework registries,
so the run grows to 900s and the job stays alive long enough to be
reclaimed by the runner. Recent focused runs failed with SIGTERM after
9-10 minutes without a single test failure, and one earlier run failed
at `internal/app 902.651s`, 2.65s past the package timeout.
Fan the same package plan across the shard matrix test-race already
uses, and run each shard the way test-race runs it: internal/app through
the process-isolating helper, cli/smoke with their wider package budget,
release-scripts without race and with archive tooling.
changed-test-packages.sh gains `list-shard`, which intersects the
impacted set with scripts/ci/test-packages.sh shard membership so shard
definitions stay single-sourced — and so an unknown shard name aborts
there rather than reporting an empty selection, which would let a
mistyped shard skip every test while reporting success.
release-scripts is in the matrix on purpose: its dedicated job only runs
at full-suite or release-sensitive scope, so omitting it here would stop
testing test/scripts changes altogether. A test pins that the shard
selections partition the impacted set exactly, so shard-plan drift
cannot silently shrink focused coverage.
Every neighbouring string in the root help listing — service
descriptions, utility descriptions, global flag usage — is hardcoded
Chinese. Routing only the feedback label through i18n therefore rendered
it in English on any host whose LANG is not zh_*, leaving a lone English
line inside an otherwise Chinese screen.
Hardcode the label and drop the two locale entries it needed. A test
assertion now pins the Chinese label so the indirection cannot return
unnoticed.
`dws --help` now closes with a Feedback section that links the
user-experience survey form, tagged with source=dws-cli so submissions
arriving through the CLI can be told apart from other channels.
The entry is deliberately root-only: this CLI is driven mostly by AI
agents, and repeating a survey link in every subcommand help would be
pure context noise. A guard test pins that boundary.
The URL is printed on its own unwrapped line — it is longer than the
help rule width, and breaking it would stop terminals from recognizing
it as a clickable hyperlink.
A universal Agent whose obsolete private copy cannot be retired installs
nothing there, yet every entry point counted that retirement failure as an
install failure — aborting `npm install`, `dws skill setup`, and the shell
installers even when the canonical store and all links published correctly,
and skipping the skills-state write. Route retirement failures to a separate
warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell,
install.sh, install-skills.sh, install-event.sh, install-devapp.sh).
Also:
- Add a checked-rename fallback for filesystems that reject the atomic
no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and
the previously unsupported platforms build and work.
- PowerShell multi-mode links only bundle skills, never the shared canonical
store, so third-party/user skills are no longer fanned into every Agent root.
- Prune ~/.dws/skill-backups to the newest 5 on every surface; encode
HOME-relative backup names on PowerShell to preserve origin.
- Add simulated-win32 junction coverage and rewrite the tautological
no-replace test; remove dead code whose tests gave false coverage.
- Soften the overstated Windows ownership-proof comment (NTFS tunneling).
Fifth-review addition: declaring InputFile silently claims the whole
@-prefixed value space, which matters in this product because at-mention
style values are common (--at-user @zhangsan would report a file read
failure), and declaring InputStdin makes a literal "-" unreachable. Both
are decided at declaration time and cannot be fixed downstream, so record
them next to the confirmation rule in the author rules.
Fourth-review fix: the FlagSpec sub-field table in the homology doc is
the named authority for "what each field does and whether it reaches
Schema parameters", and RFC §5.0.2 asserts declaration fields embed into
dws.schema.*. Input satisfied neither entry, leaving its deliberate
non-projection indistinguishable from an oversight. Add the table row and
the §5.0.2 exception note so the capability stays a declared fact (Usage
prose) rather than inviting an invented annotation.
Third-review fix for a CI blocker: run-platform-coverage-gate.sh only
executes ^(TestAllShortcuts|TestCrossPlatformCoverage) yet enforces 100%
coverage of changed production lines, so the TestResolveInputFlags names
left every new input.go statement reported as uncovered. Rename them to
the gate prefix, drop three unreachable pflag Set error branches that no
test could ever cover, and add the reachable stdin read-failure case.
Verified: changed code coverage 100.0000% (67 statements).
Second-review fix: explicitInputFlagName judged usability with an
unconditional TrimSpace while rawValue only trims when Trim is set. For
a non-Trim flag a whitespace main value is usable and shadows a changed
alias; the resolver could then rewrite the shadowed alias (and fail on
its @path) while the fallback chain still read the main value. Mirror
rawValue's usable() exactly and pin the shadow case with a regression
test whose alias path does not exist.
Self-review fixes: a Trim flag receiving " @path" judged usability on the
trimmed value (rawValue) while the source prefix check saw the raw value,
so the token would ship as a literal. Trim before the prefix check. Also
build the file-read error once with a conditional hint option, and pin
the default-value/env passthrough plus Trim edge with regression tests.
Document the landed corecmd.Input transitional form: declaration shape
(FlagSpec/LeafFlag/shortcut.Flag), runtime resolution semantics and
ordering, author rules (help prose, confirmation interaction with
stdin, construction-time validation), and the delta table against the
target typed InputSource design.
Port the lark-cli Flag.Input capability: a KindString flag may declare
Input sources ("file" for @path, "stdin" for -) and the framework
rewrites the explicit token into the payload content before
required/enum/constraint/Validate checks. @@value escapes to a literal
@value; a single stdin consumer per invocation is enforced; a leading
UTF-8 BOM is stripped. Shortcut.Flag gains the same declaration and the
adapter maps it through; LeafSpec inherits it via the LeafFlag alias.
Split chat message and group references by task, update intent routing and context budget, distinguish accepted card updates from verified writes, and explain the ambiguous chat --from flag.
- extend the silent-rollback contract to install-event.sh
- static contract: Restore-MultiSkillSet removes published paths lexically
(section-scoped so identity-anchor refactors keep the guarantee) and link
staging dirs are cleaned via Remove-LinkStageRoot / Remove-DevLinkStageRoot
- install-event.sh integration test: an uninstallable agent target is
skipped loudly while later agents still receive links
- pwsh probe: Test-SamePhysicalSkillRoot must dereference junctions and
symlinks (junction idempotency asserted where junctions are creatable)
- install.ps1: remove published junctions lexically in Restore-MultiSkillSet
(Windows PowerShell 5.1 follows reparse points during Remove-Item -Recurse
and could delete canonical store contents); clean link staging dirs
lexically in Publish-CanonicalSkillLinks and Move-SkillPathRecoverably
- install.ps1: Test-SamePhysicalSkillRoot now dereferences junctions via
Get-PhysicalSkillPath (mirrors EvalSymlinks/realpathSync/cd -P), so reruns
recognize already-published junctions instead of backup churn
- install-event.sh: replace silent 'mv ... 2>/dev/null || true' rollback with
the loud backup-retained failure contract already enforced for devapp
- event/devapp sh+ps1: link→copy fallback and per-agent failures now degrade
per agent like install.sh (skip loudly, continue, report at the end)
instead of aborting mid-loop or swallowing errors
- tests: junction-lexical removal contract, event per-agent degrade
integration test, pwsh junction physical-root recognition + rerun
idempotency (no backup churn)
The Coverage context was the PR critical path (~17 min end to end):
coverage-current re-ran the whole suite serially (-p 1, ~13 min) and
coverage-baseline re-ran it again at the merge-base (~13 min) although
that profile is a pure function of the base commit.
- coverage-current now owns only the scoped (standard-tier) profile;
full-suite candidate profiles come from a 5-way shard matrix
(app/cli/generators/helpers/remaining) that keeps -p 1 inside each
shard on isolated runners. scripts/ci/test-packages.sh list-coverage
defines the shards and verify proves the union equals the previous
single-run package set exactly once.
- the aggregate Coverage job reassembles the disjoint shard profiles
into coverage.txt before make coverage-gate, failing closed when a
shard file is missing, so gate semantics (100% changed-code +
scope-matched overall non-regression) are byte-compatible.
- coverage-baseline restores the merge-base full-suite profile from an
exact-key cache (merge-base SHA + resolved Go version) written by the
last green main push; any miss falls back to recomputing in the
merge-base worktree. Exact key only - no prefix fallback, a near-miss
profile would compare the candidate against the wrong commit.
- new contract tests pin the shard matrix, the assembly step, the
exact-key cache pair, and the absence of restore-keys; the package
plan test also covers the coverage shard partition.
- Fix Example indentation (tab -> 2 spaces)
- Remove unsubstantiated default en-US from --language help/docs
- Add test asserting calendarId/language are omitted when only --id is passed
A failed canonical publish only failed the upgrade when
hasDependentSkillRoot reported a non-universal link target; that helper
explicitly skipped universal agents, which are exactly the direct consumers
of ~/.agents/skills. On a universal-only machine (e.g. only Codex
installed), UpgradeSkillLocations* returned a nil error with nothing
installed, contradicting the documented "canonical publication is
mandatory and fails the upgrade loudly" contract.
Canonical publish failures now return an error unconditionally in both the
mono and multi branches, and hasDependentSkillRoot is removed. The test
that pinned the old standalone-does-not-fail-fast behavior now asserts
error propagation in both modes.
The allowSystemApps gate (homeDir == systemHome) was effectively a no-op in
production: systemHome came from os.UserHomeDir, which honors the $HOME env
override just like homeDir, so the two were always equal and the gate never
fired when $HOME was overridden.
ResolveSystemHomeDir now prefers the OS user database (getpwuid on Unix),
which is independent of $HOME, falling back to $HOME only when the user record
cannot be resolved. Production behavior is unchanged (a real $HOME still
matches); an isolated/overridden HOME now correctly skips machine-wide
/Applications discovery for zcode/minimax. The app surface references the same
shared resolver.
This is the correct fix for the hermeticity concern (machine-wide state leaking
into an isolated HOME): there is no cross-surface production inconsistency to
port — script installers always operate on the real user HOME in practice, so
they need no gate.
The new root-type guard shifted `filepath.WalkDir`'s outer error branch into
the diff, and neither the macOS nor the Windows runner reaches it naturally —
raising Windows coverage to 99.9365% and blocking the gate. Add a
`statusWalkDir` seam and a `TestCrossPlatformCoverage` regression that swaps
in a WalkDir returning a sentinel error, asserting it is surfaced unchanged.
Verified locally: changed code coverage back to 100.0000%.
Two follow-ups to the latest CR:
* push/sync uploads (`pushUploadFilePinned`): the PUT-time check pinned inode,
size, and mtime before dispatch but nothing rechecked the source after PUT
succeeded — only the root itself. An editor overwrite, truncate-rewrite, or
mmap-in-place during transfer would land a mixed old/new byte stream in OSS
and still be committed, corrupting the remote file in overwrite/local-wins.
Now stat the still-open handle again before `commit_upload`; any change in
inode/size/mtime aborts the commit. Post-PUT stat failures also abort.
* status root (`walkLocalTree`): `filepath.WalkDir` refuses to follow the root
when it is itself a directory symlink and reports it as a non-regular entry,
so the walker silently returned an empty local index and status flagged
every remote file as `new_remote`. Fail closed before the walk: the root
must be a real directory; symlinks and non-directories are rejected with a
clear message. A `statusRootLstat` seam keeps the rejection regressible on
platforms that cannot create directory symlinks (Windows without admin).
Both fixes come with `TestCrossPlatformCoverage*` regressions and take the
platform coverage gate from 99.9356% back to 100.0000% (1553 statements).
The Windows coverage gate reported changed-code coverage at 99.9360% because
drive_push.go:471-473 — the branch that surfaces an error passed to the
fs.WalkDir callback as its third argument — was not exercised. macOS runners
happen to exercise it via directory-lstat failures, Windows runners do not.
Add walk_callback_receives_error under
TestCrossPlatformCoverageDrivePushFinalWalkAndCommandGates, which swaps
walkPinnedLocalFS to invoke the callback with a non-nil err and asserts the
error is bubbled up unchanged.
Verified locally that the new subtest hits drive_push.go:471.17,473.4 with
count=1.
Windows keeps the pinned directory locked while a handle inside it is open
(os.Root plus the pull temp file or the upload source), so renaming that
directory fails with a sharing violation. Every "pinned root/ancestor was
swapped" reproduction in the drive mirror tests relied on such a rename, so 13
tests failed on windows-latest. That, not a coverage shortfall, is why
Coverage (Windows) exited 1 before the gate ever ran.
Each reproduction now falls back to injecting the equivalent identity change
when the rename is refused. pinnedPullRoot.verify() and verifyParent() read
current identity only through pullPathStat / pullRootLstat, so pointing those
seams at another directory hits the same fail-closed branches. Unix still
performs the real move and loses no strength.
Assertions that need an actual replacement tree now branch on the helper's
return value. forcePinnedFallbackForTest makes the fallback path itself
regressible on any platform, and a dedicated test covers it.
Verified locally with the fallback forced on: all 13 tests pass and changed
code coverage stays at 100%.
The platform coverage gate runs only TestAllShortcuts and
TestCrossPlatformCoverage*, so several changed statements had no platform
test exercising them:
- drive_pull.go: the smart-policy re-check that skips publication when the
target is refreshed in place (same inode) while the download is running.
- drive_pull.go: the post-publish verifyParent failure, where the result is
already on disk and must not be rolled back.
- drive_replace_unix.go: rename(2) replacement of an existing target; the
Windows side already had the symmetric test.
- drive_status_windows.go: the filepath.Clean rewrite guard had no input
reaching it, because isSafeRemoteSegment filters separators upstream.
macOS changed-code coverage: 99.8053% -> 100.0000% (1541 statements).
- **Command typo guidance** — returns a validation error with up to three nearest command suggestions and the parent `--help` entry instead of printing the full command list.
- **Doc and Drive parameter aliases** — normalizes reviewed identifier, pagination, path, version, and role synonyms while blocking ambiguous values before dispatch.
- **International DingTalk region support** — adds `.io` login and MCP routing, pre-release endpoint overrides, and profile-aware gateway selection while preserving the existing `.com` flow.
- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags.
- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces.
- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name.
- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write.
- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent.
- **Robot group reference replies** (#928) — `chat message send-by-bot` supports paired `--reply` and `--ref-sender` flags for Markdown replies that quote an existing group message.
- **Document write verification** (#960) — avoids false partial-success results when normalized Markdown, paginated blocks, inline images, or version reverts are confirmed by server readback. Document reverts and media inserts now require explicit readback evidence and report partial success when the server cannot prove the requested result.
- **Doc/drive description scope** — restates the `dingtalk-doc` description as document-entity-and-content operations with an explicit exclusion list, and narrows `dingtalk-drive` to file-level management of DingTalk documents, so first-round Agent selection separates content work from file management without changing CLI behavior.
- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. Batch `set-dropdown` now rejects unsupported top-level `colors` / `source-colors`; Inline colors belong in `options[].color`, while SourceRange color writes remain unsupported.
- **Sheet read completion metadata** — documents and preserves returned ranges, truncation reasons, and partial-read status for large range and CSV reads.
- **Windows event bus lifecycle** — start event consumers without unsupported inherited file descriptors, stop buses through local IPC with a termination fallback, and preserve subscription cleanup when startup fails.
- **Chat group roles** (#1058) — exposes the single-value `--role-id` flag for assigning one custom group role while preserving hidden `--role-ids` compatibility.
- **Chat user mentions** — preserves literal `<@openDingTalkId>` tokens in current-user Markdown messages and rejects mismatches between message-body mentions and mention flags before sending.
- **Chat direct media** — uses the IM upload target field for current-user direct file, audio, and video uploads, then uses the Chat receiver field for final message delivery.
- **CLI compatibility governance** — adds a reviewed two-stage path for hiding retained legacy commands or optional `NoOpt=true` boolean flags from Help and Schema when their activated capability moves to a dedicated command, with legacy-leaf, complete parameter/constant mapping, durable runtime constant evidence, protected framework bridges, dry-run preservation, parameter-collision, and fail-closed required-parameter checks.
- **OA admin approval query** — `oa approval list-by-admin` queries approval instances of a template with admin scope, with simple flags and an advanced `--request` mode; `startTime`/`endTime` use `yyyy-MM-dd HH:mm:ss` strings per the 2026-08 MCP contract update (ISO-8601 flag inputs auto-convert), and pageSize/time format are validated client-side with localized errors.
- **Chat personal emotions** — adds `chat emotion list`, `chat emotion send`, and `chat emotion favorite` for current-user personal favorite emotion listing, sending, and favoriting.
- **Minutes, DingTalk tasks, and Wiki parameter aliases** — adds reviewed parameter-name normalization, ambiguity guards, and end-to-end payload coverage for the three products.
- **Calendar empty windows** (#1074) — returns a legitimate empty result when the service emits its exact exhausted empty-event sentinel.
- **Task update verification** (#1074) — compares due-time readback as exact milliseconds so committed updates are no longer reported as failures.
- **Comment reaction validation** (#1074) — narrows accepted reaction input to reviewed DingTalk emoji names and rejects Unicode emoji and unsupported names such as `like` and `heart` before the RPC.
- **Stable release sealing** — directly preparing a stable release now renders and archives release fragments merged after its beta baseline, avoiding a forced extra beta solely to consume pending notes.
- **Whiteboard shortcuts** (#1082) — adds strict query and confirmed update workflows with stable-target receipts and exact readback verification.
- **Sheet shortcut hardening** (#1082) — makes worksheet listing and cell-range reads fail closed on malformed, ambiguous, or truncated responses, publishes a closed reviewed output shape, and preserves non-executing `--dry-run` previews for range reads.
- **AiSearch and Contact shortcuts** (#1083) — adds strict people search and reviewed unified results; people results must use the live-reviewed `person` source, and exact mobile lookups normalize accepted formatting before calling the dedicated mobile interface. Agent/public discovery keeps `contact +list-roles`, `contact +list-roster-fields`, `contact +get-roster`, and incomplete Live routes unavailable rather than publishing ambiguous results, while the historical Contact CLI commands retain legacy MCP execution and real error propagation. The legacy role-list projection preserves the service's reviewed null placeholder without exposing that ambiguous row through Agent Result contracts.
- **AITable datasource shortcuts** — adds 7 shortcuts for datasource sync management (`+datasource-create`, `+datasource-update`, `+datasource-sync`, `+datasource-sync-status`, `+datasource-get-config`, `+datasource-list-sources`, `+datasource-get-fields`) and updates the `dingtalk-aitable` skill with routing rules and a new `aitable-datasource.md` reference guide.
- **Legacy global slot recovery** — recovers a rejected identity refresh from the legacy global keychain slot when the organization mirror is absent, with strict corp/user matching so blank-user legacy tokens only recover for single-account organizations.
- **OA approval attachment upload** — `dws oa approval attachment upload --file <path>` uploads a local file as an approval attachment in one command: it initializes the upload credential (MCP `oa/init_attachment_upload_info`), HTTP PUTs the file to OSS, then commits it (MCP `oa/commit_attachment_upload_info`). `--file-name` defaults to the file's base name and `--md5` is auto-computed when omitted.
- **Sheet floating images** — supports creating or replacing a floating image directly from a local file with `create-float-image --file` and `update-float-image --file`, while retaining the existing `--src` workflow.
- **Sheet revision changesets** — adds read-only commands for querying the current workbook revision and reviewing Agent-readable changes between revisions, with guidance for distinguishing revisions from saved history versions and safely selecting rollback targets.
- Today (non-leaf): owning Cobra command → complete `corecmd.GroupPolicy{Mode, Positionals, Recovery}` → `corecmd.ApplyGroupPolicy`; the final assembled-tree gate rejects undeclared groups and stale group declarations on leaves
- **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` (`corecmd.ContractDecl`; nested fields are `contract.*`)
- Naming: `ContractDecl` is the authoring leaf declaration. "Schema" means Catalog / `ToolSpec` delivery — do not reintroduce `SchemaDecl`.
-`Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `contractfinal.RegisterRuntimeContractFinal` (all callers — `corecmd.New` registers internally); assembly **pass-throughs** Final.
@@ -60,6 +61,7 @@ Schema contract) keep separate authorities — do not merge them with
- **Tier2** — `DeclareLeafMetadata` (helpers migration; **Shortcut may also use this path — acceptable**)
- **Tier3** — bare Cobra (should shrink over time; reviewed exclusions where needed)
- Long-term outlook only: broader mcpbind / fewer hand-written `Execute` bodies. **Not** a current hard requirement to delete `Shortcut.Execute` or force mcpbind.
- Group policy is separate from the leaf tiers: `corecmd.Spec` remains leaf-only. `ApplyGroupPolicy` must not infer or enable `TraverseChildren`; parent local-flag inheritance remains an explicit owning-command surface.
- Description declare vs delivery: construction requires `ContractDecl.Description` (evidence). Catalog delivery prefers Cobra Long → provenance `cobra_help`; without Long, declared text → `contract_final`. Title: declared first, then Short, then MCP. Do **not** read this as "declare = wire final" or dual authority.
- **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority
- Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared
@@ -210,7 +210,7 @@ The verifier uses isolated directories and does not replace the `dws` on the cur
The upgrade process follows a two-phase atomic flow to ensure consistency:
1. **Prepare** — downloads the platform-specific binary and skill packages to a temporary directory, verifies SHA256 checksums, and extracts/validates all files. If any step fails, the upgrade aborts without modifying the existing installation.
2. **Apply** — only after all preparations succeed, the binary is replaced and skills are flattened into detected agent-specific roots (for example `~/.codex/skills/dingtalk-chat`). `~/.agents/skills` is used only when no specific Agent is detected; once a specific root is active, older DWS-managed generic copies are backed up and retired so the same Skill is not discovered twice.
2. **Apply** — only after all preparations succeed, the binary is replaced and skills are flattened into the canonical `~/.agents/skills` root. Agents classified by the pinned compatibility registry as supporting the universal root read it directly; other detected Agents receive links to the canonical copy, with a direct-copy fallback when links are unavailable. Older DWS-managed agent-specific copies are backed up and retired so the same Skill is not discovered twice.
A backup of the current version is automatically created before each upgrade. Use `dws upgrade --rollback` to restore the previous version if needed.
@@ -405,7 +405,7 @@ After installing, AI tools like Claude Code / Cursor can operate DingTalk direct
curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install-skills.sh | sh
```
> Installers prefer detected agent-specific roots such as`$HOME/.codex/skills/`. They use`.agents/skills/` only as the generic fallback when no specific Agent is detected; multi layout is per-product siblings, while mono uses the `dws/` subdirectory.
> Installers use`$HOME/.agents/skills/` as the canonical global store, following the universal`.agents/skills`convention. Agents classified by the pinned compatibility registry as universal read that root directly; detected non-universal Agents receive links to it (or copies when links are unavailable). Multi layout is per-product siblings, while mono uses the `dws/` subdirectory.
>
> China users: prefix `DWS_GITEE_REPO` to use the Gitee mirror — see [China mirror](#china-mirror).
<summary><strong>Personal Event Subscription</strong> — real-time DingTalk messages for event-driven agents</summary>
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog covers scoped and all one-to-one/group messages, specified senders, read/recall/reaction events, group lifecycle events, and six OA approval task/instance events.
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog covers scoped and all one-to-one/group messages, specified senders, read/recall/reaction events, group lifecycle events, and seven OA approval task/instance events.
The default `ndjson`, `json`, and `pretty` output preserves the transport envelope (`type`, `event_type`, string `data`, and `headers`) for existing scripts; `compact` retains its existing processor. Add `--flatten` to emit the stable top-level business fields used by Agent workflows. `--format` controls JSON serialization; `--flatten` controls the data structure and cannot be combined with `-f raw` or `--debug-raw-events`.
本文定义一种受控迁移:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口。迁移必须保持原 flag 的 requiredness:optional 只能迁到 optional,required 只能迁到 required。它只解决这一种精确变更,不是通用 breaking-change 豁免。
本文定义两种受控 flag 迁移:
1. `flag_rename`:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口;rename 必须保持原 flag 的 requiredness,optional 只能迁到 optional,required 只能迁到 required。
2. `requiredness_change`:同一个公开 flag 从 optional 精确提升为 required;flag 的名称、类型、作用域、可见性、shorthand、`no_opt` 与 alias 关系必须保持不变。
_Group chats, conversations, messages, and robot/webhook integrations._
**23 commands**
**26 commands**
| Command | Description | When to use |
|---|---|---|
| `dws chat bot search` | Search robots (bots) created by the current user by keyword. | When the agent needs to resolve one of its own bots by name to a robot code before sending bot messages. |
| `dws chat conversation-info` | Retrieve basic metadata for a conversation (single chat or group chat) by conversation ID. | When the agent needs context about a conversation (name, type, member count) before operating on it. |
| `dws chat emotion favorite` | Add a media ID to the current user's personal favorite emotions. | When the agent needs to save an available mediaId as a reusable personal emotion, optionally preserving source message context. |
| `dws chat emotion list` | List the current user's personal favorite emotions. | When the agent needs to inspect available personal emotions or resolve an emotionId/mediaId before sending. |
| `dws chat emotion send` | Send a personal favorite emotion to a group or direct chat as the authenticated user. | When the agent needs to send a known personal emotion mediaId to exactly one group, userId, or openDingTalkId target. |
| `dws chat group create` | Create a new internal group chat with a set of initial members. | When the agent needs to spin up a dedicated group for a new project, incident, or discussion thread. |
| `dws chat group members` | List members of a group chat; can also be used against the current user to enumerate their groups' members. | When the agent needs the roster of a group before mentioning, removing, or auditing members. |
| `dws chat group members add` | Add one or more users to an existing group chat. | When the agent expands a group to include additional participants. |
This guide explains how to log in to the international DingTalk region and run DWS commands against `*.dingtalk.io` services.
## Region behavior
- `dws auth login --intl` creates or refreshes an international login using the `.io` login, OAuth, and MCP services.
- Omitting `--intl` keeps the existing domestic `.com` behavior.
- `--intl` is a login option, not a global option for business commands. After login, commands such as `contact`, `calendar`, and `doc` derive the region from the selected Token/profile.
- Each new Token records its login region. Switching profiles therefore switches the official DingTalk gateway region automatically.
- `--international` is a compatibility alias. Prefer `--intl` in new scripts.
For the complete Chinese guide, see [DWS 国际版(DingTalk `.io`)使用手册](./international-region-guide.zh-CN.md).
## Check availability
```bash
dws auth login --help
```
The help output must include `--intl` and `--international`.
When validating a source checkout, build it first and use `./dws` so an older binary on `PATH` is not invoked accidentally:
```bash
make build
./dws auth login --help
```
## Log in
Browser login:
```bash
dws auth login --intl
```
Device flow for SSH, containers, and headless environments:
```bash
dws auth login --intl --device
```
User OAuth with custom application credentials:
```bash
dws auth login --intl \
--client-id <APP_KEY> \
--client-secret <APP_SECRET>
```
This mode still requires the user to complete OAuth authorization in a browser; it is not a userless `client_credentials` login. The application must be configured on the international developer platform with the required callback and permissions. Never commit an AppSecret to source control or include it in logs.
## Verify the login
```bash
dws auth status --format json
dws profile list --format json
dws contact user get-self
```
The last command is a read-only smoke check. If the organization has not enabled CLI access, an organization administrator must enable it or approve the access request on the international developer platform.
## Use domestic and international profiles together
```bash
# Domestic (.com)
dws auth login
# International (.io)
dws auth login --intl
# Find the stable profile selectors
dws profile list --format json
```
Persistently switch profiles:
```bash
dws profile switch <corpId>:<userId>
```
Toggle back to the previous profile:
```bash
dws profile switch -
```
Select a profile for one command without changing the default:
```bash
dws --profile <corpId>:<userId> contact user get-self
```
Do not add `--intl` to business commands. DWS routes official endpoints from the selected profile's Token region.
## Isolated smoke testing
Use a separate configuration directory to avoid changing the normal `~/.dws` login state:
A corresponding `pre-mcp.*` URL is also accepted, and DWS derives the paired `pre-login.*` / `pre-mcp.*` bases. `--mcp-url` explicitly overrides the MCP base URL for that login.
Pre-release services may require internal network access or allowlisted accounts. `--pre-url` is intended primarily for the MCP-managed credential flow. Do not combine it with direct custom `--client-id/--client-secret` mode unless the pre-release API contract explicitly supports that combination.
## Troubleshooting
### The browser still opens a `.com` page
1. Run `dws auth login --help` and confirm `--intl` is present.
2. For a source checkout, use `./dws` instead of an older installed binary.
3. Confirm the executed command is `dws auth login --intl`.
### A business command appears to use the wrong region
Run `dws profile list --format json`, then switch with the exact `<corpId>:<userId>` selector or use the global `--profile` option. For a legacy Token created before region metadata existed, reauthorize it with `dws auth login --intl` for an international account or `dws auth login` for a domestic account.
### Login succeeds but the command reports missing permission
This normally means the organization has not enabled CLI access or the application lacks a required permission. It does not by itself indicate a region-routing failure.
### Should I edit `~/.dws/mcp_url` manually?
No. Normal users should establish the login with `dws auth login` or `dws auth login --intl`. DWS then routes official endpoints from the selected Token/profile. Manual configuration is reserved for maintainers who explicitly control the target environment.
## Command reference
| Scenario | Command |
|---|---|
| Domestic browser login | `dws auth login` |
| International browser login | `dws auth login --intl` |
function filter(){const text=q.value.trim().toLowerCase(),group=g.value;rows.forEach(r=>{const okText=!text||r.textContent.toLowerCase().includes(text),okGroup=!group||r.children[0].textContent===group;r.style.display=okText&&okGroup?'':'none'})}q.addEventListener('input',filter);g.addEventListener('change',filter);
if_,_,err:=authCoverageRunLogin(t,nil,"table",true,map[string]string{"token":"token","intl":"true"});err==nil||!strings.Contains(err.Error(),"failed to persist MCP URL"){
// DirectRuntimeProductIDs returns product IDs that should stay visible for
// direct runtime execution. Dynamic products come from MCP discovery/plugin
// registration; built-in helper products such as devapp resolve their endpoint
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.