dsh-d365-kit 0.34.1 → 0.34.4
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.
- package/CHANGELOG.md +319 -308
- package/CHANGELOG.zh-CN.md +318 -307
- package/INSTALL.md +94 -94
- package/INSTALL.zh-CN.md +93 -93
- package/LICENSE +21 -21
- package/README.md +336 -327
- package/README.zh-CN.md +336 -327
- package/client.js +1818 -1818
- package/cordis.patch.yml +44 -44
- package/docs/en/architecture.md +172 -172
- package/docs/en/checks.md +184 -184
- package/docs/en/installation.md +151 -151
- package/docs/en/permissions.md +161 -161
- package/docs/en/scenarios.md +202 -202
- package/docs/en/tools.md +665 -665
- package/docs/en/troubleshooting.md +131 -131
- package/docs/zh/architecture.md +172 -172
- package/docs/zh/checks.md +184 -184
- package/docs/zh/installation.md +151 -151
- package/docs/zh/permissions.md +161 -161
- package/docs/zh/scenarios.md +202 -202
- package/docs/zh/tools.md +665 -665
- package/docs/zh/troubleshooting.md +131 -131
- package/examples/prompts.en.md +206 -206
- package/examples/prompts.zh-CN.md +206 -206
- package/examples/waivers.example.json +20 -20
- package/icon.svg +6 -6
- package/index.js +139 -139
- package/lib/auth.js +181 -181
- package/lib/client.js +496 -496
- package/lib/code-check.js +549 -549
- package/lib/config.js +323 -323
- package/lib/contract-history.js +49 -49
- package/lib/contract.js +583 -583
- package/lib/csharp-check.js +493 -493
- package/lib/data-migrate.js +1318 -1318
- package/lib/data-policy.js +78 -78
- package/lib/error-codes.js +80 -80
- package/lib/gitlab.js +161 -161
- package/lib/health.js +205 -205
- package/lib/migrate.js +732 -732
- package/lib/preflight.js +116 -116
- package/lib/refs.js +191 -191
- package/lib/resources.js +320 -320
- package/lib/retention-defaults.js +17 -17
- package/lib/retention.js +384 -384
- package/lib/runtime.js +232 -232
- package/lib/settings-http.js +512 -512
- package/lib/settings-store.js +371 -371
- package/lib/snapshot.js +149 -149
- package/lib/tool-code-check.js +133 -133
- package/lib/tool-configure.js +169 -169
- package/lib/tool-contract.js +376 -376
- package/lib/tool-data-sync.js +559 -559
- package/lib/tool-data-write.js +203 -203
- package/lib/tool-data.js +282 -282
- package/lib/tool-diff.js +484 -484
- package/lib/tool-environments.js +106 -106
- package/lib/tool-flows.js +540 -540
- package/lib/tool-health.js +127 -127
- package/lib/tool-layers.js +497 -497
- package/lib/tool-metadata.js +417 -417
- package/lib/tool-migrate.js +102 -102
- package/lib/tool-refs.js +206 -206
- package/lib/tool-reports.js +299 -299
- package/lib/tool-sync.js +430 -430
- package/lib/tool-write.js +563 -563
- package/lib/waivers.js +83 -83
- package/lib/write-actions.js +1374 -1374
- package/lib/write-policy.js +199 -199
- package/lib/write-store.js +167 -167
- package/locale/en.json +68 -68
- package/locale/zh.json +68 -68
- package/package.json +2 -2
- package/skills/d365-troubleshooting/SKILL.md +115 -115
package/docs/en/installation.md
CHANGED
|
@@ -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.
|
|
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`:
|
|
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.
|
|
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.
|
|
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.4"
|
|
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.4"
|
|
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.4"
|
|
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.
|
package/docs/en/permissions.md
CHANGED
|
@@ -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.
|