dsh-d365-kit 0.34.1 → 0.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +318 -308
  2. package/CHANGELOG.zh-CN.md +317 -307
  3. package/INSTALL.md +94 -94
  4. package/INSTALL.zh-CN.md +93 -93
  5. package/LICENSE +21 -21
  6. package/README.md +336 -327
  7. package/README.zh-CN.md +336 -327
  8. package/client.js +1818 -1818
  9. package/cordis.patch.yml +44 -44
  10. package/docs/en/architecture.md +172 -172
  11. package/docs/en/checks.md +184 -184
  12. package/docs/en/installation.md +151 -151
  13. package/docs/en/permissions.md +161 -161
  14. package/docs/en/scenarios.md +202 -202
  15. package/docs/en/tools.md +665 -665
  16. package/docs/en/troubleshooting.md +131 -131
  17. package/docs/zh/architecture.md +172 -172
  18. package/docs/zh/checks.md +184 -184
  19. package/docs/zh/installation.md +151 -151
  20. package/docs/zh/permissions.md +161 -161
  21. package/docs/zh/scenarios.md +202 -202
  22. package/docs/zh/tools.md +665 -665
  23. package/docs/zh/troubleshooting.md +131 -131
  24. package/examples/prompts.en.md +206 -206
  25. package/examples/prompts.zh-CN.md +206 -206
  26. package/examples/waivers.example.json +20 -20
  27. package/icon.svg +6 -6
  28. package/index.js +139 -139
  29. package/lib/auth.js +181 -181
  30. package/lib/client.js +496 -496
  31. package/lib/code-check.js +549 -549
  32. package/lib/config.js +323 -323
  33. package/lib/contract-history.js +49 -49
  34. package/lib/contract.js +583 -583
  35. package/lib/csharp-check.js +493 -493
  36. package/lib/data-migrate.js +1318 -1318
  37. package/lib/data-policy.js +78 -78
  38. package/lib/error-codes.js +80 -80
  39. package/lib/gitlab.js +161 -161
  40. package/lib/health.js +205 -205
  41. package/lib/migrate.js +732 -732
  42. package/lib/preflight.js +116 -116
  43. package/lib/refs.js +191 -191
  44. package/lib/resources.js +320 -320
  45. package/lib/retention-defaults.js +17 -17
  46. package/lib/retention.js +384 -384
  47. package/lib/runtime.js +232 -232
  48. package/lib/settings-http.js +512 -512
  49. package/lib/settings-store.js +371 -371
  50. package/lib/snapshot.js +149 -149
  51. package/lib/tool-code-check.js +133 -133
  52. package/lib/tool-configure.js +169 -169
  53. package/lib/tool-contract.js +376 -376
  54. package/lib/tool-data-sync.js +559 -559
  55. package/lib/tool-data-write.js +203 -203
  56. package/lib/tool-data.js +282 -282
  57. package/lib/tool-diff.js +484 -484
  58. package/lib/tool-environments.js +106 -106
  59. package/lib/tool-flows.js +540 -540
  60. package/lib/tool-health.js +127 -127
  61. package/lib/tool-layers.js +497 -497
  62. package/lib/tool-metadata.js +417 -417
  63. package/lib/tool-migrate.js +102 -102
  64. package/lib/tool-refs.js +206 -206
  65. package/lib/tool-reports.js +299 -299
  66. package/lib/tool-sync.js +430 -430
  67. package/lib/tool-write.js +563 -563
  68. package/lib/waivers.js +83 -83
  69. package/lib/write-actions.js +1374 -1374
  70. package/lib/write-policy.js +199 -199
  71. package/lib/write-store.js +167 -167
  72. package/locale/en.json +68 -68
  73. package/locale/zh.json +68 -68
  74. package/package.json +2 -2
  75. package/skills/d365-troubleshooting/SKILL.md +115 -115
@@ -1,151 +1,151 @@
1
- # Installation & Configuration
2
-
3
- **English** | [简体中文](../zh/installation.md)
4
-
5
- ## Prerequisites
6
-
7
- | Item | Requirement |
8
- | --- | --- |
9
- | DSH | Desktop edition, able to open the Web GUI (the settings page is a Web client plugin) |
10
- | Network | Able to reach the environment domain (`*.crm*.dynamics.com` or the matching region) and the sign-in endpoint |
11
- | Application user | An app registration in Entra ID plus an application user created in Dataverse, giving you `ClientId` / `ClientSecret` / `TenantId` |
12
- | Permissions | Read metadata, read solutions (`prvReadSolution`), read custom APIs (`prvReadCustomAPI`), read the layer table (`prvReadmsdyn_componentlayer`) |
13
-
14
- A read-only application user is enough; for the write capabilities, messages such as `CreateAttribute` require the matching permission on that user in the environment (system customizer, or the matching privileges ticked in a custom role).
15
-
16
- > Once the application user is created in one environment, create it again in **every** target environment (the same ClientId can be reused).
17
-
18
- ## Installation
19
-
20
- ### Option 1: install from npm (shortest)
21
-
22
- ```bash
23
- plugin_manager install_bundle target="dsh-d365-kit@0.34.1"
24
- ```
25
-
26
- Works once the package is published to the registry pnpm is configured against — public npm or an internal mirror. The package has no scope, so no scope mapping is needed. To install a specific version pin it as `dsh-d365-kit@<version>`.
27
-
28
- > **GitHub alone is not a registry.** A repository on GitHub is reachable directly through the `git+https://...` spec (option 2), and plain `npm install` can also take `github:<user>/<repo>#<tag>`; but `install_bundle target="dsh-d365-kit"` only resolves after the package is published to a registry. The repo ships `.github/workflows/publish.yml`: configure the `NPM_TOKEN` secret, push a `v*` tag, and the package is published for you.
29
-
30
- ### Option 2: install from Git (no registry needed)
31
-
32
- ```bash
33
- plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.1"
34
- ```
35
-
36
- DSH pulls the package from git with pnpm. After installing, check:
37
-
38
- ```
39
- plugin_manager list_plugins
40
- ```
41
-
42
- Success means `include:dsh-d365-kit` is present with `enabled=true` and `fiberPhase=active`.
43
-
44
- ### Option 3: install from a local directory
45
-
46
- Put the whole repository directory anywhere on this machine:
47
-
48
- ```bash
49
- plugin_manager install_bundle target="D:\path\to\dsh-d365-kit"
50
- ```
51
-
52
- This is a `link:` install: code changes take effect immediately (the Host side still needs a reload), and upgrading means running `git pull` yourself.
53
-
54
- ### Option 4: zip
55
-
56
- Unzip it, then install it as in option 3.
57
-
58
- ### A restart is required after installing
59
-
60
- Host-side tools work as soon as the plugin is installed; the **settings page** belongs to the Web client plugin, and DSH caches the client plugin module graph, so DSH must be restarted before the page is loaded.
61
-
62
- After restarting, open Settings and you should see "D365 Toolkit". If it is missing, run `d365_envs` first to confirm the Host side has loaded, then check the entry name in `plugin_manager list_plugins`.
63
-
64
- ## First-time configuration
65
-
66
- 1. Settings → D365 Toolkit → **Environments** → Add environment:
67
-
68
- | Field | Description |
69
- | --- | --- |
70
- | key | Short name the tools use, e.g. `dev`, `sit`. **Once it goes into a plan it is recorded in the audit log, so pick a stable name** |
71
- | URL | Environment URL, e.g. `https://contoso-dev.crm5.dynamics.com` |
72
- | Cloud | `public` / `usgovernment` / `dod` / `china`; determines the sign-in endpoint |
73
- | Tenant ID | Entra tenant GUID |
74
- | ClientId | Application (client) ID of the app registration |
75
- | Client secret | Secret value generated in the app registration |
76
-
77
- 2. Click "Test connection"; an organization name and version number coming back means the credentials are correct.
78
- 3. **Save**. The secret goes into `d365.secrets.json`, everything else into `d365.config.json`.
79
- 4. The **Permissions** tab lists capabilities per environment: "Read metadata" is on by default (turn it off and tables, fields and views become unreadable); tick "Read business data" to read records (off by default).
80
- 5. To write, tick the matching capabilities and fill in "Default solution for changes" (the unique name of an **existing unmanaged** solution).
81
-
82
- ## Where the configuration lives
83
-
84
- The environment list and tool parameters live in `configFile`:
85
-
86
- ```
87
- <workspace>/d365.config.json # environment list, default environment, tool parameters, permissions
88
- <workspace>/d365.secrets.json # holds only clientSecret and gitlabToken (plaintext)
89
- ```
90
-
91
- `configFile` is resolved in this order:
92
-
93
- 1. `configFile` in `cordis.patch.yml` (an absolute path, machine-specific configuration from install time)
94
- 2. The `D365_CONFIG_FILE` environment variable
95
- 3. `<USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json` (when that directory exists)
96
- 4. `<current working directory>\d365.config.json`
97
-
98
- The repository's `cordis.patch.yml` carries no `configFile`, so the default falls through to item 3; when the workspace is elsewhere, set it with the environment variable or the patch.
99
- `d365.secrets.json` always sits in the same directory as `configFile`.
100
-
101
- **Do not commit these two files to Git**: the repository's `.gitignore` already covers them and `.d365/`.
102
-
103
- ## Credential resolution order
104
-
105
- Each value (`tenantId` / `clientId` / `clientSecret`) is taken in this order:
106
-
107
- 1. The inline value in the environment configuration;
108
- 2. The `clientSecret` the settings page writes into `d365.secrets.json` (an overlay);
109
- 3. The environment variable name given by `<kind>Env` in that environment's configuration, e.g. `"clientSecretEnv": "D365_DEV_SECRET"`;
110
- 4. The derived name `D365_<ENV>_<KIND>`, e.g. `D365_dev_CLIENT_SECRET` (the key as-is, hyphens included);
111
- 5. The shared names `D365_TENANT_ID` / `D365_CLIENT_ID` / `D365_CLIENT_SECRET`.
112
-
113
- Production is better served by environment variables; the settings page is enough for local debugging.
114
-
115
- Tokens are cached in process memory and keyed by a fingerprint of "URL + tenant + ClientId + secret" — changed credentials are fetched immediately, with no restart.
116
-
117
- ## Verification
118
-
119
- ```
120
- d365_envs check=true
121
- d365_metadata resource=entities top=3
122
- ```
123
-
124
- If both return content, it works. The first `d365_sync` pulls the full metadata (about 125 MB, three minutes on a real environment); if you would rather not use the disk, skip it and query on demand with `d365_metadata`.
125
-
126
- ## Upgrade
127
-
128
- ```bash
129
- # installed from Git
130
- plugin_manager remove_bundle target=dsh-d365-kit
131
- plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.1"
132
- ```
133
-
134
- After changing the version number you must **remove first, then install**: link/pnpm returns `already-installed` when it believes the package exists, and does not replace the code.
135
-
136
- Restart DSH after upgrading (the settings page code has to re-enter the module graph).
137
-
138
- ## Uninstall
139
-
140
- ```bash
141
- plugin_manager remove_bundle target=dsh-d365-kit
142
- ```
143
-
144
- Local data (`d365.config.json`, `d365.secrets.json`, `.d365/`) is not deleted; clean it up manually when needed.
145
-
146
- ## Sharing with a team
147
-
148
- - Everyone installs a copy and fills in their own `d365.config.json` and secrets;
149
- - **Permissions are granted per machine**: ticks on someone else's machine do not affect yours, and vice versa;
150
- - Reports and snapshots live in each person's workspace and are not shared;
151
- - To give the whole team the same environment definitions, put the `environments` from `d365.config.json` into an internal document — each person still fills in their own secrets.
1
+ # Installation & Configuration
2
+
3
+ **English** | [简体中文](../zh/installation.md)
4
+
5
+ ## Prerequisites
6
+
7
+ | Item | Requirement |
8
+ | --- | --- |
9
+ | DSH | Desktop edition, able to open the Web GUI (the settings page is a Web client plugin) |
10
+ | Network | Able to reach the environment domain (`*.crm*.dynamics.com` or the matching region) and the sign-in endpoint |
11
+ | Application user | An app registration in Entra ID plus an application user created in Dataverse, giving you `ClientId` / `ClientSecret` / `TenantId` |
12
+ | Permissions | Read metadata, read solutions (`prvReadSolution`), read custom APIs (`prvReadCustomAPI`), read the layer table (`prvReadmsdyn_componentlayer`) |
13
+
14
+ A read-only application user is enough; for the write capabilities, messages such as `CreateAttribute` require the matching permission on that user in the environment (system customizer, or the matching privileges ticked in a custom role).
15
+
16
+ > Once the application user is created in one environment, create it again in **every** target environment (the same ClientId can be reused).
17
+
18
+ ## Installation
19
+
20
+ ### Option 1: install from npm (shortest)
21
+
22
+ ```bash
23
+ plugin_manager install_bundle target="dsh-d365-kit@0.34.3"
24
+ ```
25
+
26
+ Works once the package is published to the registry pnpm is configured against — public npm or an internal mirror. The package has no scope, so no scope mapping is needed. To install a specific version pin it as `dsh-d365-kit@<version>`.
27
+
28
+ > **GitHub alone is not a registry.** A repository on GitHub is reachable directly through the `git+https://...` spec (option 2), and plain `npm install` can also take `github:<user>/<repo>#<tag>`; but `install_bundle target="dsh-d365-kit"` only resolves after the package is published to a registry. The repo ships `.github/workflows/publish.yml`: once either the `NPM_TRUSTED_PUBLISHING` variable or the `NPM_TOKEN` secret exists, every push to `main` with a new `package.json` version is published for you.
29
+
30
+ ### Option 2: install from Git (no registry needed)
31
+
32
+ ```bash
33
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
34
+ ```
35
+
36
+ DSH pulls the package from git with pnpm. After installing, check:
37
+
38
+ ```
39
+ plugin_manager list_plugins
40
+ ```
41
+
42
+ Success means `include:dsh-d365-kit` is present with `enabled=true` and `fiberPhase=active`.
43
+
44
+ ### Option 3: install from a local directory
45
+
46
+ Put the whole repository directory anywhere on this machine:
47
+
48
+ ```bash
49
+ plugin_manager install_bundle target="D:\path\to\dsh-d365-kit"
50
+ ```
51
+
52
+ This is a `link:` install: code changes take effect immediately (the Host side still needs a reload), and upgrading means running `git pull` yourself.
53
+
54
+ ### Option 4: zip
55
+
56
+ Unzip it, then install it as in option 3.
57
+
58
+ ### A restart is required after installing
59
+
60
+ Host-side tools work as soon as the plugin is installed; the **settings page** belongs to the Web client plugin, and DSH caches the client plugin module graph, so DSH must be restarted before the page is loaded.
61
+
62
+ After restarting, open Settings and you should see "D365 Toolkit". If it is missing, run `d365_envs` first to confirm the Host side has loaded, then check the entry name in `plugin_manager list_plugins`.
63
+
64
+ ## First-time configuration
65
+
66
+ 1. Settings → D365 Toolkit → **Environments** → Add environment:
67
+
68
+ | Field | Description |
69
+ | --- | --- |
70
+ | key | Short name the tools use, e.g. `dev`, `sit`. **Once it goes into a plan it is recorded in the audit log, so pick a stable name** |
71
+ | URL | Environment URL, e.g. `https://contoso-dev.crm5.dynamics.com` |
72
+ | Cloud | `public` / `usgovernment` / `dod` / `china`; determines the sign-in endpoint |
73
+ | Tenant ID | Entra tenant GUID |
74
+ | ClientId | Application (client) ID of the app registration |
75
+ | Client secret | Secret value generated in the app registration |
76
+
77
+ 2. Click "Test connection"; an organization name and version number coming back means the credentials are correct.
78
+ 3. **Save**. The secret goes into `d365.secrets.json`, everything else into `d365.config.json`.
79
+ 4. The **Permissions** tab lists capabilities per environment: "Read metadata" is on by default (turn it off and tables, fields and views become unreadable); tick "Read business data" to read records (off by default).
80
+ 5. To write, tick the matching capabilities and fill in "Default solution for changes" (the unique name of an **existing unmanaged** solution).
81
+
82
+ ## Where the configuration lives
83
+
84
+ The environment list and tool parameters live in `configFile`:
85
+
86
+ ```
87
+ <workspace>/d365.config.json # environment list, default environment, tool parameters, permissions
88
+ <workspace>/d365.secrets.json # holds only clientSecret and gitlabToken (plaintext)
89
+ ```
90
+
91
+ `configFile` is resolved in this order:
92
+
93
+ 1. `configFile` in `cordis.patch.yml` (an absolute path, machine-specific configuration from install time)
94
+ 2. The `D365_CONFIG_FILE` environment variable
95
+ 3. `<USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json` (when that directory exists)
96
+ 4. `<current working directory>\d365.config.json`
97
+
98
+ The repository's `cordis.patch.yml` carries no `configFile`, so the default falls through to item 3; when the workspace is elsewhere, set it with the environment variable or the patch.
99
+ `d365.secrets.json` always sits in the same directory as `configFile`.
100
+
101
+ **Do not commit these two files to Git**: the repository's `.gitignore` already covers them and `.d365/`.
102
+
103
+ ## Credential resolution order
104
+
105
+ Each value (`tenantId` / `clientId` / `clientSecret`) is taken in this order:
106
+
107
+ 1. The inline value in the environment configuration;
108
+ 2. The `clientSecret` the settings page writes into `d365.secrets.json` (an overlay);
109
+ 3. The environment variable name given by `<kind>Env` in that environment's configuration, e.g. `"clientSecretEnv": "D365_DEV_SECRET"`;
110
+ 4. The derived name `D365_<ENV>_<KIND>`, e.g. `D365_dev_CLIENT_SECRET` (the key as-is, hyphens included);
111
+ 5. The shared names `D365_TENANT_ID` / `D365_CLIENT_ID` / `D365_CLIENT_SECRET`.
112
+
113
+ Production is better served by environment variables; the settings page is enough for local debugging.
114
+
115
+ Tokens are cached in process memory and keyed by a fingerprint of "URL + tenant + ClientId + secret" — changed credentials are fetched immediately, with no restart.
116
+
117
+ ## Verification
118
+
119
+ ```
120
+ d365_envs check=true
121
+ d365_metadata resource=entities top=3
122
+ ```
123
+
124
+ If both return content, it works. The first `d365_sync` pulls the full metadata (about 125 MB, three minutes on a real environment); if you would rather not use the disk, skip it and query on demand with `d365_metadata`.
125
+
126
+ ## Upgrade
127
+
128
+ ```bash
129
+ # installed from Git
130
+ plugin_manager remove_bundle target=dsh-d365-kit
131
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
132
+ ```
133
+
134
+ After changing the version number you must **remove first, then install**: link/pnpm returns `already-installed` when it believes the package exists, and does not replace the code.
135
+
136
+ Restart DSH after upgrading (the settings page code has to re-enter the module graph).
137
+
138
+ ## Uninstall
139
+
140
+ ```bash
141
+ plugin_manager remove_bundle target=dsh-d365-kit
142
+ ```
143
+
144
+ Local data (`d365.config.json`, `d365.secrets.json`, `.d365/`) is not deleted; clean it up manually when needed.
145
+
146
+ ## Sharing with a team
147
+
148
+ - Everyone installs a copy and fills in their own `d365.config.json` and secrets;
149
+ - **Permissions are granted per machine**: ticks on someone else's machine do not affect yours, and vice versa;
150
+ - Reports and snapshots live in each person's workspace and are not shared;
151
+ - To give the whole team the same environment definitions, put the `environments` from `d365.config.json` into an internal document — each person still fills in their own secrets.
@@ -1,161 +1,161 @@
1
- # Permissions & Writes
2
-
3
- **English** | [简体中文](../zh/permissions.md)
4
-
5
- ## Default posture
6
-
7
- Right after installation the plugin is **read-only**:
8
-
9
- - Metadata (entities, fields, forms, views, plugins, solutions, layers, sitemaps…) is governed by `metadata.read`, which is **on by default** — an environment that is not listed explicitly counts as granted;
10
- - **Business records** are not readable by default; grant `data.read` per environment;
11
- - Any change needs the matching capability granted per environment, and every execution needs a human confirmation.
12
-
13
- "What you can see" and "what you can change" share one per-environment capability table, because the two belong together. The page renders one block per environment, separated by a rule.
14
-
15
- ## Capability table
16
-
17
- Configured in `d365.config.json`:
18
-
19
- ```json
20
- {
21
- "write": {
22
- "capabilityVersion": 2,
23
- "environments": {
24
- "dev": ["metadata.read", "data.read", "optionValue.add", "attribute.add", "view.columns"],
25
- "sit": ["metadata.read", "data.read"],
26
- "uat": []
27
- },
28
- "solutionUniqueName": "ContosoBase"
29
- }
30
- }
31
- ```
32
-
33
- - An environment with **no entry** has never been configured and keeps the default (only `metadata.read`);
34
- - An environment whose entry is an **empty array** has everything revoked, including metadata;
35
- - `capabilityVersion` is written by the settings page when it saves. Older files (without the field) get `metadata.read` added back to their existing entries, which preserves the pre-upgrade "metadata is always readable" behaviour.
36
-
37
- | Capability | Real HTTP operation | Group | Effect |
38
- | --- | --- | --- | --- |
39
- | `metadata.read` | metadata `GET` | Metadata | Read tables/fields/forms/views/plugins/layers (on by default) |
40
- | `data.read` | business-data `GET` | Data | Read business records |
41
- | `data.write` | `POST` / `PATCH` | Data | Create or update business records (high risk, no deletions) |
42
- | `optionValue.add` | `InsertOptionValue` | Option set | Add option values (global or field-local) |
43
- | `optionValue.updateLabel` | `UpdateOptionValue` | Option set | Change option labels |
44
- | `optionValue.delete` | `DeleteOptionValue` | Option set | **Permanently delete** option values (high risk) |
45
- | `attribute.add` | `POST EntityDefinitions(...)/Attributes` | Field | Add fields |
46
- | `attribute.update` | `PUT` field URL (`MSCRM.MergeLabels`) | Field | Change display name, description, required level |
47
- | `attribute.delete` | `DELETE` field URL | Field | **Permanently delete** the field and its data (high risk) |
48
- | `entity.update` | `PUT` entity URL (`MSCRM.MergeLabels`) | Table | Change table display name and description |
49
- | `view.columns` | `PATCH savedqueries(id)` | View | Add or remove columns in system views (`layoutxml` only) |
50
- | `layer.remove` | `RemoveActiveCustomizations` | Layer | **Permanently remove** the top-most unmanaged layer of a component (high risk; tables, fields, global option sets, forms, views, workflows, web resources, plug-ins, custom APIs) |
51
- | `message.invoke` | any `POST` action | Message | Call one Dataverse message. The message is implemented by the target environment's plug-ins, so it may write data, send notifications or call external systems (high risk; every call is confirmed in the chat) |
52
-
53
- Tick them per environment under Permissions in the settings page; "Copy to all environments" is supported.
54
-
55
- ## Three gates, one decision point
56
-
57
- The decision lives in one place, `lib/client.js`, so a new tool cannot bypass it — every request (including "test connection" in the settings page) goes through it:
58
-
59
- - **Metadata read**: the entity set name in the request URL is on the metadata allowlist (`EntityDefinitions`, `systemforms`, `savedqueries`, `webresourceset`, `solutions`, `msdyn_componentlayers`…) and requires `metadata.read`; it fails with `metadata-read-forbidden`;
60
- - **Business-data read**: every other `GET` is blocked as business data and requires `data.read`; it fails with `data-read-forbidden`;
61
- - **Write**: a request that is not `GET` and does not belong to a metadata message (`PublishXml`, `InsertOptionValue`, `CreateAttribute`, `RemoveActiveCustomizations`…) must have `data.write` first, otherwise it fails with `data-write-forbidden`. A `d365_invoke` `POST` goes through `message.invoke` instead (failing as `message-invoke-forbidden`), while its `GET` function is still a read and needs `data.read`.
62
-
63
- Every gate fires **before a token is acquired**, so the request never reaches the environment. Decisions are cached per environment for 2 seconds, so a change in the settings page applies to the next tool call.
64
-
65
- ## Two-step writes
66
-
67
- ### Step 1: generate a plan (read-only)
68
-
69
- `plan-*` reads the current state, skips items with no change, writes the operations to a plan file (`.d365/pending-writes/`) and returns the operation list.
70
-
71
- | Plan | Constraint |
72
- | --- | --- |
73
- | `plan-add-options` | Dedupes by the code before `\|`; new values continue from the current maximum +1; existing codes go to `skipped` |
74
- | `plan-relabel` | Only items whose label actually changes go into the plan |
75
- | `plan-option-delete` | Requires `acknowledge: true`; missing values go to `skipped`, and Dataverse refuses the delete when existing records reference the value |
76
- | `plan-add-attribute` | The table must exist and the field must not; `SchemaName` must carry the publisher prefix |
77
- | `plan-update-attribute` | Only items that really change are included |
78
- | `plan-delete-attribute` | Requires `acknowledge: true`, otherwise it is rejected outright |
79
- | `plan-update-entity` | Changes the display name / description only |
80
- | `plan-view-columns` | Locates the system view by view name (same name distinguished by `entity`); only the cell list in `layoutxml` changes, filter conditions are left alone |
81
-
82
- Local option sets (an option set carried by a field) are addressed with `entity` + `attribute`; global option sets with `optionSet`.
83
-
84
- ### Step 2: execute (needs a human)
85
-
86
- ```
87
- Run mw04aul6-1ci6aa
88
- ```
89
-
90
- The agent calls `d365_write action=execute planId=...`, which:
91
-
92
- 1. Reads the plan and checks that its status is `pending`;
93
- 2. Checks that the environment **still** holds this capability;
94
- 3. Checks whether the environment is importing/publishing (see below);
95
- 4. Shows a confirmation card: environment, targets, operation count, the first 10 operations; with deletions it adds an unrecoverable warning;
96
- 5. Click "Execute now" → operations are written one by one → publish runs in the background → the run is audited.
97
-
98
- You can also review and execute from the Pending changes tab in the settings page (the same second, red confirmation). Both entry points share the same checks.
99
-
100
- ### Plan states
101
-
102
- `pending → running → done / failed`
103
-
104
- `running` is persisted before the first write, so a process killed midway never leaves behind a plan that still looks clickable. For a plan that is not `pending`, both the tool and the settings page **refuse** to execute or cancel it — writes are not idempotent, and a rerun would resubmit the parts that already took effect. To redo the work, generate a new plan.
105
-
106
- ### Pre-flight check
107
-
108
- Dataverse holds a customization lock while importing or publishing. Microsoft first-party updates arrive as solution imports, and an environment spends a large part of the day doing exactly that — during which writes either fail with `0x80071151` or wait for the lock until they time out.
109
-
110
- So before executing, the plugin looks for rows in `msdyn_solutionhistories` where `msdyn_endtime` is null:
111
-
112
- | Case | Behavior |
113
- | --- | --- |
114
- | A row exists and its start time is within `busyWindowMinutes` (default 120 minutes) | Refuse to execute and report the operation in progress (`environment-busy`) |
115
- | Only older rows (leftover zombie rows) | Do not block; mention it in the result |
116
- | The table cannot be queried (no permission) | Skip the check and note in the result that it was skipped |
117
-
118
- `force: true` skips it; use it only when you are sure the row is a leftover.
119
-
120
- ### Default solution
121
-
122
- Every metadata change is recorded in some **solution layer**. Without a solution it goes into the active default layer (Default Solution), which does not travel with an exported solution, and downstream this shows up as "the field does not exist".
123
-
124
- So fill in "Default solution for changes": the unique name of an **existing unmanaged** solution. It is validated at plan time — a name that does not exist fails with `unknown-solution`, a managed one with `managed-solution`, instead of waiting until execution to fail.
125
-
126
- ### Publish scope
127
-
128
- After writing, only the **affected components** are published:
129
-
130
- - Option set operations → the corresponding option set (field-local option set → the table that owns it);
131
- - Field / table / view operations → the table that owns them.
132
-
133
- The payload looks like:
134
-
135
- ```xml
136
- <importexportxml>
137
- <entities><entity>account</entity></entities>
138
- <optionsets><optionset>contoso_taxcategoryos</optionset></optionsets>
139
- </importexportxml>
140
- ```
141
-
142
- One publish takes seconds to a dozen seconds, instead of the minutes `PublishAllXml` needs to recompute two thousand-plus tables. Only when the affected components cannot be derived does it fall back to a full publish. Publishing happens in the background, the result is written to `.d365/changes/publish-<planId>.json`, and the execute response carries `publishState` (`skipped` / `running`).
143
-
144
- ## Writing business data
145
-
146
- Three constraints on `d365_data_write`:
147
-
148
- - It needs `data.write`, off by default;
149
- - It can write to **one** environment only;
150
- - Every write shows a confirmation card; the request goes out only after approval.
151
-
152
- Writes need no publish (business data is not metadata). The audit records field names only, never values. There is no delete.
153
-
154
- ## Safety boundaries
155
-
156
- - **Write endpoints accept only the settings page**: `/write/policy`, `/write/cancel`, `/write/execute`, `/write/publish` require an `x-d365-meta-write: 1` header. A cross-site page must pass a CORS preflight to send that header, and these routes never answer preflights; if the request carries an `Origin`, it must also be same-origin. The auth channel rewrites the URL to `http://dsh.internal` and swallows `Origin`, so Origin alone cannot be the test. Rejected requests are logged to `.d365/write-guard.log`.
157
- - **Retries only for safe methods**: `GET`/`HEAD` retry on network errors and 5xx; `POST`/`PATCH`/`DELETE` retry only on **429** (the server explicitly did not execute). A write timeout is reported as "may have taken effect", for a human to check in the environment, rather than being silently resent.
158
- - **Republishing passes the same gate**: the environment must still hold the capability it had at the time, and an idle check runs first (a busy environment returns 409 rather than a raw 429).
159
- - **Cancel rejects non-`pending` just like execute**: there is no "the page says cancelled while the backend keeps writing".
160
- - **No rollback**: metadata deletions cannot be undone, so a delete requires `acknowledge: true`, and the card calls it out separately.
161
- - Write endpoints expose no bulk script entry point and no "skip confirmation" switch; `force` skips only the idle check, never the confirmation.
1
+ # Permissions & Writes
2
+
3
+ **English** | [简体中文](../zh/permissions.md)
4
+
5
+ ## Default posture
6
+
7
+ Right after installation the plugin is **read-only**:
8
+
9
+ - Metadata (entities, fields, forms, views, plugins, solutions, layers, sitemaps…) is governed by `metadata.read`, which is **on by default** — an environment that is not listed explicitly counts as granted;
10
+ - **Business records** are not readable by default; grant `data.read` per environment;
11
+ - Any change needs the matching capability granted per environment, and every execution needs a human confirmation.
12
+
13
+ "What you can see" and "what you can change" share one per-environment capability table, because the two belong together. The page renders one block per environment, separated by a rule.
14
+
15
+ ## Capability table
16
+
17
+ Configured in `d365.config.json`:
18
+
19
+ ```json
20
+ {
21
+ "write": {
22
+ "capabilityVersion": 2,
23
+ "environments": {
24
+ "dev": ["metadata.read", "data.read", "optionValue.add", "attribute.add", "view.columns"],
25
+ "sit": ["metadata.read", "data.read"],
26
+ "uat": []
27
+ },
28
+ "solutionUniqueName": "ContosoBase"
29
+ }
30
+ }
31
+ ```
32
+
33
+ - An environment with **no entry** has never been configured and keeps the default (only `metadata.read`);
34
+ - An environment whose entry is an **empty array** has everything revoked, including metadata;
35
+ - `capabilityVersion` is written by the settings page when it saves. Older files (without the field) get `metadata.read` added back to their existing entries, which preserves the pre-upgrade "metadata is always readable" behaviour.
36
+
37
+ | Capability | Real HTTP operation | Group | Effect |
38
+ | --- | --- | --- | --- |
39
+ | `metadata.read` | metadata `GET` | Metadata | Read tables/fields/forms/views/plugins/layers (on by default) |
40
+ | `data.read` | business-data `GET` | Data | Read business records |
41
+ | `data.write` | `POST` / `PATCH` | Data | Create or update business records (high risk, no deletions) |
42
+ | `optionValue.add` | `InsertOptionValue` | Option set | Add option values (global or field-local) |
43
+ | `optionValue.updateLabel` | `UpdateOptionValue` | Option set | Change option labels |
44
+ | `optionValue.delete` | `DeleteOptionValue` | Option set | **Permanently delete** option values (high risk) |
45
+ | `attribute.add` | `POST EntityDefinitions(...)/Attributes` | Field | Add fields |
46
+ | `attribute.update` | `PUT` field URL (`MSCRM.MergeLabels`) | Field | Change display name, description, required level |
47
+ | `attribute.delete` | `DELETE` field URL | Field | **Permanently delete** the field and its data (high risk) |
48
+ | `entity.update` | `PUT` entity URL (`MSCRM.MergeLabels`) | Table | Change table display name and description |
49
+ | `view.columns` | `PATCH savedqueries(id)` | View | Add or remove columns in system views (`layoutxml` only) |
50
+ | `layer.remove` | `RemoveActiveCustomizations` | Layer | **Permanently remove** the top-most unmanaged layer of a component (high risk; tables, fields, global option sets, forms, views, workflows, web resources, plug-ins, custom APIs) |
51
+ | `message.invoke` | any `POST` action | Message | Call one Dataverse message. The message is implemented by the target environment's plug-ins, so it may write data, send notifications or call external systems (high risk; every call is confirmed in the chat) |
52
+
53
+ Tick them per environment under Permissions in the settings page; "Copy to all environments" is supported.
54
+
55
+ ## Three gates, one decision point
56
+
57
+ The decision lives in one place, `lib/client.js`, so a new tool cannot bypass it — every request (including "test connection" in the settings page) goes through it:
58
+
59
+ - **Metadata read**: the entity set name in the request URL is on the metadata allowlist (`EntityDefinitions`, `systemforms`, `savedqueries`, `webresourceset`, `solutions`, `msdyn_componentlayers`…) and requires `metadata.read`; it fails with `metadata-read-forbidden`;
60
+ - **Business-data read**: every other `GET` is blocked as business data and requires `data.read`; it fails with `data-read-forbidden`;
61
+ - **Write**: a request that is not `GET` and does not belong to a metadata message (`PublishXml`, `InsertOptionValue`, `CreateAttribute`, `RemoveActiveCustomizations`…) must have `data.write` first, otherwise it fails with `data-write-forbidden`. A `d365_invoke` `POST` goes through `message.invoke` instead (failing as `message-invoke-forbidden`), while its `GET` function is still a read and needs `data.read`.
62
+
63
+ Every gate fires **before a token is acquired**, so the request never reaches the environment. Decisions are cached per environment for 2 seconds, so a change in the settings page applies to the next tool call.
64
+
65
+ ## Two-step writes
66
+
67
+ ### Step 1: generate a plan (read-only)
68
+
69
+ `plan-*` reads the current state, skips items with no change, writes the operations to a plan file (`.d365/pending-writes/`) and returns the operation list.
70
+
71
+ | Plan | Constraint |
72
+ | --- | --- |
73
+ | `plan-add-options` | Dedupes by the code before `\|`; new values continue from the current maximum +1; existing codes go to `skipped` |
74
+ | `plan-relabel` | Only items whose label actually changes go into the plan |
75
+ | `plan-option-delete` | Requires `acknowledge: true`; missing values go to `skipped`, and Dataverse refuses the delete when existing records reference the value |
76
+ | `plan-add-attribute` | The table must exist and the field must not; `SchemaName` must carry the publisher prefix |
77
+ | `plan-update-attribute` | Only items that really change are included |
78
+ | `plan-delete-attribute` | Requires `acknowledge: true`, otherwise it is rejected outright |
79
+ | `plan-update-entity` | Changes the display name / description only |
80
+ | `plan-view-columns` | Locates the system view by view name (same name distinguished by `entity`); only the cell list in `layoutxml` changes, filter conditions are left alone |
81
+
82
+ Local option sets (an option set carried by a field) are addressed with `entity` + `attribute`; global option sets with `optionSet`.
83
+
84
+ ### Step 2: execute (needs a human)
85
+
86
+ ```
87
+ Run mw04aul6-1ci6aa
88
+ ```
89
+
90
+ The agent calls `d365_write action=execute planId=...`, which:
91
+
92
+ 1. Reads the plan and checks that its status is `pending`;
93
+ 2. Checks that the environment **still** holds this capability;
94
+ 3. Checks whether the environment is importing/publishing (see below);
95
+ 4. Shows a confirmation card: environment, targets, operation count, the first 10 operations; with deletions it adds an unrecoverable warning;
96
+ 5. Click "Execute now" → operations are written one by one → publish runs in the background → the run is audited.
97
+
98
+ You can also review and execute from the Pending changes tab in the settings page (the same second, red confirmation). Both entry points share the same checks.
99
+
100
+ ### Plan states
101
+
102
+ `pending → running → done / failed`
103
+
104
+ `running` is persisted before the first write, so a process killed midway never leaves behind a plan that still looks clickable. For a plan that is not `pending`, both the tool and the settings page **refuse** to execute or cancel it — writes are not idempotent, and a rerun would resubmit the parts that already took effect. To redo the work, generate a new plan.
105
+
106
+ ### Pre-flight check
107
+
108
+ Dataverse holds a customization lock while importing or publishing. Microsoft first-party updates arrive as solution imports, and an environment spends a large part of the day doing exactly that — during which writes either fail with `0x80071151` or wait for the lock until they time out.
109
+
110
+ So before executing, the plugin looks for rows in `msdyn_solutionhistories` where `msdyn_endtime` is null:
111
+
112
+ | Case | Behavior |
113
+ | --- | --- |
114
+ | A row exists and its start time is within `busyWindowMinutes` (default 120 minutes) | Refuse to execute and report the operation in progress (`environment-busy`) |
115
+ | Only older rows (leftover zombie rows) | Do not block; mention it in the result |
116
+ | The table cannot be queried (no permission) | Skip the check and note in the result that it was skipped |
117
+
118
+ `force: true` skips it; use it only when you are sure the row is a leftover.
119
+
120
+ ### Default solution
121
+
122
+ Every metadata change is recorded in some **solution layer**. Without a solution it goes into the active default layer (Default Solution), which does not travel with an exported solution, and downstream this shows up as "the field does not exist".
123
+
124
+ So fill in "Default solution for changes": the unique name of an **existing unmanaged** solution. It is validated at plan time — a name that does not exist fails with `unknown-solution`, a managed one with `managed-solution`, instead of waiting until execution to fail.
125
+
126
+ ### Publish scope
127
+
128
+ After writing, only the **affected components** are published:
129
+
130
+ - Option set operations → the corresponding option set (field-local option set → the table that owns it);
131
+ - Field / table / view operations → the table that owns them.
132
+
133
+ The payload looks like:
134
+
135
+ ```xml
136
+ <importexportxml>
137
+ <entities><entity>account</entity></entities>
138
+ <optionsets><optionset>contoso_taxcategoryos</optionset></optionsets>
139
+ </importexportxml>
140
+ ```
141
+
142
+ One publish takes seconds to a dozen seconds, instead of the minutes `PublishAllXml` needs to recompute two thousand-plus tables. Only when the affected components cannot be derived does it fall back to a full publish. Publishing happens in the background, the result is written to `.d365/changes/publish-<planId>.json`, and the execute response carries `publishState` (`skipped` / `running`).
143
+
144
+ ## Writing business data
145
+
146
+ Three constraints on `d365_data_write`:
147
+
148
+ - It needs `data.write`, off by default;
149
+ - It can write to **one** environment only;
150
+ - Every write shows a confirmation card; the request goes out only after approval.
151
+
152
+ Writes need no publish (business data is not metadata). The audit records field names only, never values. There is no delete.
153
+
154
+ ## Safety boundaries
155
+
156
+ - **Write endpoints accept only the settings page**: `/write/policy`, `/write/cancel`, `/write/execute`, `/write/publish` require an `x-d365-meta-write: 1` header. A cross-site page must pass a CORS preflight to send that header, and these routes never answer preflights; if the request carries an `Origin`, it must also be same-origin. The auth channel rewrites the URL to `http://dsh.internal` and swallows `Origin`, so Origin alone cannot be the test. Rejected requests are logged to `.d365/write-guard.log`.
157
+ - **Retries only for safe methods**: `GET`/`HEAD` retry on network errors and 5xx; `POST`/`PATCH`/`DELETE` retry only on **429** (the server explicitly did not execute). A write timeout is reported as "may have taken effect", for a human to check in the environment, rather than being silently resent.
158
+ - **Republishing passes the same gate**: the environment must still hold the capability it had at the time, and an idle check runs first (a busy environment returns 409 rather than a raw 429).
159
+ - **Cancel rejects non-`pending` just like execute**: there is no "the page says cancelled while the backend keeps writing".
160
+ - **No rollback**: metadata deletions cannot be undone, so a delete requires `acknowledge: true`, and the card calls it out separately.
161
+ - Write endpoints expose no bulk script entry point and no "skip confirmation" switch; `force` skips only the idle check, never the confirmation.