Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/dingtalk-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: DingTalk account authentication package

on:
pull_request:
paths:
- 'plugins/dingtalk/**'
- '.github/workflows/dingtalk-package.yml'
push:
branches: [main]
paths:
- 'plugins/dingtalk/**'
- '.github/workflows/dingtalk-package.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
package:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v6
with:
python-version: '3.12'
- uses: actions/setup-go@v6
with:
go-version: '1.25.9'
cache: false
- name: Verify vendored build source inventory
run: uv run --no-project python plugins/dingtalk/.wework-build/dws-auth/vendor.py --check .
- name: Test package assembly and source inventory
run: uv run --no-project python -m unittest discover -s plugins/dingtalk/.wework-build/dws-auth/tests -v
- name: Test the shared Python SDK
run: uv run --no-project python -m unittest discover -s plugins/dingtalk/.wework-build/plugin-auth/tests -v
- name: Test native provider storage and shared transport
run: uv run --no-project python plugins/dingtalk/.wework-build/dws-auth/build.py --test --output .ci-artifacts/host/dws-account-auth
- name: Build all targets and verify the real packaged entry on this OS
run: uv run --no-project python plugins/dingtalk/.wework-build/dws-auth/package.py --plugin plugins/dingtalk --output .ci-artifacts/dingtalk-account-auth.zip
- name: Retain the candidate artifact and checksum
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: dingtalk-candidate-${{ github.sha }}
path: |
.ci-artifacts/dingtalk-account-auth.zip
.ci-artifacts/dingtalk-account-auth.zip.sha256
if-no-files-found: error
retention-days: 7

release-artifact:
needs: package
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/download-artifact@v4
with:
name: dingtalk-candidate-${{ github.sha }}
path: release
- name: Verify the selected candidate checksum
working-directory: release
run: sha256sum --check dingtalk-account-auth.zip.sha256
- name: Retain the artifact after all three OS jobs pass
uses: actions/upload-artifact@v4
with:
name: dingtalk-account-auth-${{ github.sha }}
path: release/
if-no-files-found: error
retention-days: 14
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ __pycache__/
.tmp/
.idea/
.vscode/
.ci-artifacts/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ The `Windows compatibility` GitHub Actions workflow additionally parses every
PowerShell script and runs the native Windows authorization/exit-code tests on
`windows-latest`.

## DingTalk builds

DingTalk 0.3.1 declares its build in `plugins/dingtalk/.wework-build.json` and keeps
all canonical SDK inputs inside `.wework-build/` in that plugin directory.
GitHub-to-internal-repository mirroring therefore retains everything needed by
the existing MR, package, test and automatic-release pipeline. No separate
account-authentication edition or manual artifact selection is needed.

```bash
uv run --no-project --python 3.12 python plugins/dingtalk/.wework-build/dws-auth/vendor.py --check .
uv run --no-project --python 3.12 python plugins/dingtalk/.wework-build/dws-auth/package.py --plugin plugins/dingtalk --output .ci-artifacts/dingtalk-account-auth.zip
```

Maintain generated inputs in Wegent and refresh them with `sdk/dws-auth/vendor.py`.
The builder prepares pinned Go tools, preserves reviewed source, builds five native
targets and exercises the private adapter. The ordinary official publishing command
automatically runs this build; GitLab tests and releases the same resulting ZIP.
The GitHub workflow remains an additional cross-platform build check.

## Adding a plugin

1. Create `plugins/<slug>/` with a valid `.codex-plugin/plugin.json`.
Expand Down
49 changes: 41 additions & 8 deletions plugins/dingtalk/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dingtalk",
"version": "0.2.11",
"description": "通过本机已安装并完成认证的 DWS CLI,使用钉钉协作与办公能力。",
"version": "0.3.3",
"description": "通过 DWS 使用钉钉协作与办公能力。",
"author": {
"name": "Wegent"
},
Expand All @@ -22,22 +22,55 @@
"authPolicy": "on_install",
"localAuth": {
"kind": "browser_oauth",
"health": ["scripts/local-auth.sh", "health"],
"start": ["scripts/local-auth.sh", "login"],
"logout": ["scripts/local-auth.sh", "logout"],
"health": [
"scripts/local-auth.sh",
"health"
],
"start": [
"scripts/local-auth.sh",
"login"
],
"logout": [
"scripts/local-auth.sh",
"logout"
],
"timeoutSeconds": 300,
"logoutOnUninstall": false,
"tool": {
"id": "dws",
"source": "bundled"
}
},
"accountAuth": {
"protocolVersion": 1,
"credentialType": "oauth2",
"adapter": "scripts/account-auth.py",
"oauth2": [
"refresh",
"revoke"
],
"exportMode": "exclusive",
"localEnvironment": {
"DWS_CONFIG_DIR": {
"type": "directory"
},
"DWS_KEYCHAIN_DIR": {
"type": "directory"
},
"DWS_DISABLE_KEYCHAIN": {
"type": "enum",
"values": [
"1"
]
}
}
}
}
],
"interface": {
"displayName": "钉钉",
"shortDescription": "通过本机 DWS 管理钉钉协作与办公能力",
"longDescription": "自动准备跨平台 DingTalk Workspace CLI,在本机浏览器完成 OAuth 授权,并支持 AI 表格、日历、通讯录、群聊、待办、审批、考勤、日志、DING、文档、云盘、AI 听记、邮箱、在线表格、知识库和开放平台文档等能力。凭据仅由本机 DWS 管理,不上传 Wegent Backend。",
"shortDescription": "通过 DWS 管理钉钉协作与办公能力",
"longDescription": "通过 DingTalk Workspace CLI 使用 AI 表格、日历、通讯录、群聊、待办、审批、考勤、日志、DING、文档、云盘、AI 听记、邮箱、在线表格、知识库和开放平台文档等能力。",
"developerName": "Wegent",
"category": "协作",
"capabilities": [
Expand All @@ -52,7 +85,7 @@
"composerIcon": "./assets/app-icon.svg",
"logo": "./assets/app-icon.svg",
"defaultPrompt": [
"请自动检查本机 DWS CLI 和钉钉登录状态,然后帮我处理钉钉中的日程、消息、文档、待办或其他办公任务。"
"请帮我处理钉钉中的日程、消息、文档、待办或其他办公任务。"
]
}
}
4 changes: 4 additions & 0 deletions plugins/dingtalk/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Preserve the exact bytes used by the vendored SDK inventories on Windows.
.wework-build/** text eol=lf
scripts/wegent_plugin_auth/** text eol=lf
scripts/account-auth.py text eol=lf
7 changes: 7 additions & 0 deletions plugins/dingtalk/.wework-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"entrypoint": ".wework-build/dws-auth/package.py",
"outputs": [
"scripts/native"
]
}
48 changes: 48 additions & 0 deletions plugins/dingtalk/.wework-build/account-auth-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"schemaVersion": 1,
"source": "Wegent/sdk",
"files": {
"dws-auth/README.en.md": "d2444a6f3205d7c0a3e311531314d34d770f739907093a8cbc5b34e900231ae4",
"dws-auth/README.md": "939ba7cadf43782f54ded24b60b68ccb6a1b7a343c430af203818237201953af",
"dws-auth/auth-overlay/wegent_transfer.go": "fe57595cb51c8eb2f06fe3a1d084213aa0e11268db49d454d77233cffddf8f23",
"dws-auth/auth-overlay/wegent_transfer_sync_unix.go": "2a6bb4db9e5047abe4c8993de22f2d54354a423e4e5916d5f8ef18d018836581",
"dws-auth/auth-overlay/wegent_transfer_sync_windows.go": "b4fc3b971f34593a66785bbaebb7366b803b1bf8ece416b63c4801e4e73d5bcc",
"dws-auth/auth-overlay/wegent_transfer_test.go": "8a0065ea875aaf99f3187690de28d7317c76cfa622df5d2cdb34ea0714323ccc",
"dws-auth/build.py": "30c4651d42e421d6609af1b3ae89e14b136d79941298f7f8ebc10ca638ae102a",
"dws-auth/entry.py": "b27e18738846468e2a7af409b069cf575950943798e61b0088b4167d01704770",
"dws-auth/overlay/main.go": "18c40aa02f77e6a3e9be14c48d91c8112f59173324abd747643052ef6233e482",
"dws-auth/overlay/provider.go": "b453ef0449b9183d211a09ad68092adb82ac3df49d29a0c212b42ffb86a7ba80",
"dws-auth/overlay/provider_test.go": "e5dafac08e23b55e70ddd2b86c8ff2039b843dbdc0dd334badacb2b4e10adfb5",
"dws-auth/package.py": "881ccb3e3c4df06b84fe67cc24b6b4b8391f81e0e35b699cc3a5db57d22242c8",
"dws-auth/tests/test_package.py": "db7584e15bd51014947d05d75478750805e5abfdd623ad23f02ec882e1ccc9db",
"dws-auth/tests/test_toolchain.py": "c2306b7ffa0b52505f5cfff09096ad1449bb7e6c059e59dcd192a4c59b1ee889",
"dws-auth/tests/test_vendor.py": "f297ebe66383355d62a335ac901407e5487d929b86c2ddcb95cf88f87bc3a2de",
"dws-auth/toolchain.py": "c265669d702c9529036bd088061ce9375ceefd83b3901cc3910b95c43e941232",
"dws-auth/vendor.py": "14b5288c8fcf43e0365876c3afab94b8249fc9bc035319c153f9c1e54e1da821",
"dws-auth/verify.py": "72dcc844f2b1089e8d2ec073fd1e5d46b643d83d51f5d0f8622a7240f759dcd4",
"plugin-auth-go/LICENSE": "e7e430d6f6693d9e1fe09961d873d7c8f6938bfd57746d60d18b895c62b15760",
"plugin-auth-go/README.en.md": "361c47abf41b0d68572abedbad7ee0b016c7a48284fec2b726b7d593dbbfcff7",
"plugin-auth-go/README.md": "461633b5574774831ca63d581a2a130e53a91a3db89bf91e5bd8d45c8c5aaa06",
"plugin-auth-go/adapter.go": "236b56b3519007cfaa878cfc918c1efa639799b992f4a8bb97845b7871129691",
"plugin-auth-go/configuration.go": "5875beffda456b1a8b04ade81b9c40043ff7abca3cbec6b9cc65efa6da6170fd",
"plugin-auth-go/configuration_test.go": "c85ff0195bf4545eb67ec966916b99ca9cc1d2f25bb9b40ee7b6cc7f6aa7678e",
"plugin-auth-go/go.mod": "fb02c6a825a34760138dc8abd7e3b0ceef11199dd2e2eb07cd4031e8f4d4428b",
"plugin-auth-go/transport.go": "ca7e8998f2c77bfc06d5f2ab49c6ffc760af03d83ae9d69ec6f51b5147a87e76",
"plugin-auth-go/transport_test.go": "c0ec7112078c8f62b7f06e6d2fbe03270a24ddf40c89cfa5ad5b5ca736480d2a",
"plugin-auth/LICENSE": "e7e430d6f6693d9e1fe09961d873d7c8f6938bfd57746d60d18b895c62b15760",
"plugin-auth/README.en.md": "beb365b9a7e53d77a9c07be21e8c0e5c90c8d7d620b88eb09cf40600fd4776cf",
"plugin-auth/README.md": "ca0c2ddd6793ea1bb4eb00eb27ab81313545896a745c80368cc771a7a133c278",
"plugin-auth/templates/account-auth.py.tmpl": "b2683e640b18fa0670b54afc58db23f9fd4af419c13cba662c15574ac839d8d0",
"plugin-auth/templates/auth_provider.py.tmpl": "11ed9b480458c2aa5ede47bb11a2ab5579ba249cf26303a2a073da4eda5da497",
"plugin-auth/templates/cli.py.tmpl": "55c607be225c4b84583e1fcd19e1f374e53fce25799cb0826a7fb3905a38c863",
"plugin-auth/templates/oauth-provider.inc": "bb72f261c81cadcc002db2d6001a5de01c829fbf6dd855fef3da2ab203d207a2",
"plugin-auth/tests/test_runtime.py": "f041ed94bf045ef6894ca04d915f07c5b4ffff768bf6b703af106cedcc9a02d9",
"plugin-auth/tests/test_sdk.py": "e7c7ec429aeeddca7e487d143017b3eff7b26c3e602a3881510e60c9561e6ced",
"plugin-auth/tool.py": "d6d07a370ffd9a120b1a72ae08ba3cde8cc2b765bbdd59bc54943c6980e5cfa0",
"plugin-auth/wegent_plugin_auth/__init__.py": "b6ce9a286c0a06ecfe0652d5045e488eba98bcc2aad41f5ebd50e4b3aa28c98c",
"plugin-auth/wegent_plugin_auth/adapter.py": "c51549ca0e1503f6d714a52bdf6ddc265e4067aa8d4470e5c6c9077d10f5d8e6",
"plugin-auth/wegent_plugin_auth/configuration.py": "0bb293d2c82db21c5eb19a88cea884fa758700c4350e93baa238b87c5d5e08ae",
"plugin-auth/wegent_plugin_auth/runtime.py": "3fbe06a3334acf36fe9687cc9dfbdc802d804982b51ddf064880ec68e3adc84d",
"plugin-auth/wegent_plugin_auth/transport.py": "664e4a848c9fea879f729a967191c37d7ab02a0180c0f02bced4cd62c4199c34"
}
}
133 changes: 133 additions & 0 deletions plugins/dingtalk/.wework-build/dws-auth/README.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
sidebar_position: 1
---

# DWS account authentication adapter

The installed package declares source directories `DWS_CONFIG_DIR` and
`DWS_KEYCHAIN_DIR`, plus the public `DWS_DISABLE_KEYCHAIN=1` switch, through
`accountAuth.localEnvironment`. The host validates these settings and supplies
them only to local authentication callbacks. The Python SDK entry explicitly
passes them to the official adapter so migration reads the selected source store.
Unset settings retain upstream defaults; directories must exist and be absolute.
Settings are not uploaded to the backend or supplied to cloud business, refresh
or revocation callbacks.

Build the native companion from pinned DWS `v1.0.58` source plus Wegent extensions.
The source archive SHA-256 is
`f6b2dcf16b34492d7be25ce63fc81c7155d6a857af21c0604ae16bf4fa96f1e2`.
`overlay/` reuses upstream edition hooks, OAuth refresh, revocation and business
commands. `auth-overlay/` adds a function inside the upstream auth package to use
its existing refresh lock and exact-account deletion. Shared framing lives in
`../plugin-auth-go`.

```bash
uv run --project backend python sdk/dws-auth/build.py \
--output executor/target/dws-auth/dws-account-auth --test
```

`--source-archive` accepts an already downloaded archive without bypassing checksum
verification. `GOOS` / `GOARCH` select cross-compilation targets. Outputs include
the binary, LICENSE, NOTICE, and a JSON inventory containing upstream/source/binary
hashes and target platform. Tests use isolated upstream storage and synthetic
HTTP providers, never personal Keychain entries or real DingTalk accounts.

## Exclusive refresh ownership

The plugin declaration must use `accountAuth.exportMode: "exclusive"`:

1. Export the local MCP OAuth account into encrypted backend escrow without a
usable connection or device grant.
2. Detach compares the snapshot under the upstream refresh lock.
3. Persist a receipt containing no tokens; remove only that account and its
upstream mirrors; verify the grant is absent; persist the completed receipt.
4. The native host confirms detachment. One backend transaction activates the
connection, grants the source device and removes escrow ciphertext.

The receipt recovers a crash after deletion; backend confirmation is idempotent.
Refresh races durably fence off the old ID under the provider lock before the
native host cancels escrow. A user retry exports current credentials with a new
ID; delayed operations cannot delete credentials or activate the old snapshot.
Changed accounts and uncertain storage states still fail closed. Only
MCP OAuth is supported; direct-mode client secrets are not imported. Business
execution receives access credentials only and uses memory-backed store hooks.

## Build the complete plugin

```bash
uv run --project backend python sdk/dws-auth/package.py \
--plugin ../wework-plugins-public/plugins/dingtalk \
--output executor/target/dws-auth/dingtalk-account-auth.zip
```

The source declares `accountAuth` and includes the SDK. The packager preserves
those reviewed inputs and builds macOS arm64/amd64, Linux arm64/amd64 and Windows amd64 companions.
It rejects symlinks, verifies binary hashes and license files, then runs the real
packaged private entry on the build host. Health must succeed; wrong connector
identity and business credentials containing a refresh token must fail without
creating local account state. Failure preserves any previous output artifact.

Packages retain the existing 50 MiB upload and 200 MiB expanded limits. The public
CLI and 24 Python helpers now delegate through the public SDK. Readiness checks
for transferred accounts do not initiate DWS login.

## CI and official distribution

The plugin-local `.wework-build.json` declares a Python build entry and generated
directories. All inputs live under `plugins/dingtalk/.wework-build/`, so selecting
and mirroring the plugin into an internal MR retains the complete build contract.

```bash
uv run --project backend python sdk/dws-auth/vendor.py ../wework-plugins-public
uv run --project backend python sdk/dws-auth/vendor.py --check ../wework-plugins-public
uv run --no-project python sdk/plugin-build/vendor.py ../wework-plugins
```

The existing `package_plugin → unit_linux → release_plugin` pipeline remains the
publication path. Packaging runs the declared builder and prepares checksum-pinned
Go tools when needed. Tests exercise the extracted ZIP and retain its tested digest.
Release uploads that exact artifact; plugins without declarations keep source packaging.

Every reviewed source byte and mode must survive unchanged; only declared generated
directories may be added. Backend validates the protected commit, MR and input tree,
then independently verifies the successful GitLab package artifact and test digest
receipt. Missing outputs, altered sources, substituted artifacts or missing successful
tests block release. Builds receive no release credentials or personal auth environment.

Local publication uses the same entry and automatically chooses the build:

```bash
cd backend
uv run python scripts/publish_official_plugin.py ../../wework-plugins-public/plugins/dingtalk --slug dingtalk --visibility public --dry-run
```

Remove `--dry-run` to publish to the configured market. Batch seeding also builds
automatically. `--prebuilt --sha256` remains an operator artifact-import option,
not a required everyday publication step.

## Delivery boundary

Only the default DingTalk MCP provider is supported. A source with a custom
`mcp_url` is rejected before credential export. Memory execution also isolates
device-local DWS configuration so it cannot change the token recipient.

Source tests cover recovery, exact-account deletion, refresh-race rejection and
real upstream business commands. macOS builds and Windows/Linux cross-compilation
have been checked; native Windows storage and real-provider acceptance remain.
The source plugin manifest does not enable `accountAuth`; artifacts produced by
the packager do. Build and official distribution entry points are connected;
a minimal business fixture built with the same packager, launcher and five native
targets passed real publishing, installation, Backend/Electron/cloud-executor account
status checks: denied before a grant, successful after granting, denied after revocation.
Separate Electron verification used upstream code to create an isolated encrypted
source store. Desktop migration removed only the selected source grant, preserved
another account and retained the connection after reload. Evidence:
`wework/test-results/ai-verify/2026-09-07T12-19-29-787Z-40328/dws-source-qa.json`.
All credentials are synthetic; no personal keychain was accessed. Real providers,
system Keychain/DPAPI, remote CI and actual publication remain unverified.
The registered desktop checkpoint also passed upstream source-store migration,
cloud grant, business execution and revocation. All 20 flags passed; evidence:
`wework/test-results/desktop-e2e/2026-09-07T12-20-59-116Z-47125/`.
Generic SDK transfer
recovery has passed real Backend/Electron/cloud-executor E2E with a synthetic provider. A source build is not
a published, user-ready plugin.
Loading
Loading