@salesforce/afv-skills 1.40.0 → 1.41.0
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/package.json +1 -1
- package/skills/automation-flow-generate/SKILL.md +32 -43
- package/skills/experience-portal-create/SKILL.md +497 -0
- package/skills/experience-portal-create/assets/report-template.md +30 -0
- package/skills/experience-portal-create/references/mcp-invocation.md +288 -0
- package/skills/experience-portal-create/references/post-creation-activate-publish.md +165 -0
- package/skills/experience-portal-create/references/templates.md +253 -0
- package/skills/experience-ui-bundle-features-generate/SKILL.md +5 -1
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -0
- package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -0
- package/skills/platform-datamask-run/SKILL.md +345 -0
- package/skills/platform-datamask-run/references/api-surface.md +130 -0
- package/skills/platform-datamask-run/references/policy-authoring.md +185 -0
- package/skills/platform-datamask-run/references/run-and-abort.md +116 -0
- package/skills/platform-datamask-run/scripts/poll-job.sh +115 -0
- package/skills/platform-dataspace-access-configure/SKILL.md +51 -3
- package/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh +56 -0
- package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +1 -0
- package/skills/platform-sandbox-configure/SKILL.md +17 -2
- package/skills/platform-trial-org-create/SKILL.md +175 -0
- package/skills/platform-trial-org-create/examples/create_request.json +9 -0
- package/skills/platform-trial-org-create/examples/error_response.json +41 -0
- package/skills/platform-trial-org-create/examples/success_response.json +27 -0
- package/skills/platform-trial-org-create/references/error_codes.md +42 -0
- package/skills/platform-trial-org-create/references/signup_request_fields.md +69 -0
- package/skills/platform-trial-org-create/scripts/create_signup_request.sh +175 -0
- package/skills/platform-trial-org-create/scripts/get_signup_request.sh +155 -0
- package/skills/platform-widget-generate/SKILL.md +47 -6
- package/skills/platform-widget-generate/examples/conditional.json +3 -3
- package/skills/platform-widget-generate/examples/list-with-foreach.json +2 -2
- package/skills/platform-widget-generate/examples/single-object.json +2 -2
- package/skills/platform-widget-generate/references/widget-bundle-layout.md +1 -1
- package/skills/service-agentforce-channel-configure/SKILL.md +271 -0
- package/skills/service-agentforce-channel-configure/references/agent-wiring.md +97 -0
- package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +145 -0
- package/skills/service-agentforce-channel-configure/references/channel-branch-voice.md +69 -0
- package/skills/service-agentforce-channel-configure/references/channel-types.md +61 -0
- package/skills/service-agentforce-channel-configure/references/live-traffic-gate.md +86 -0
- package/skills/service-agentforce-channel-configure/references/queue-resolution.md +135 -0
- package/skills/service-agentforce-channel-configure/references/routing-flow.md +384 -0
- package/skills/service-catalog-template-deploy/SKILL.md +310 -0
- package/skills/service-catalog-template-deploy/references/cli-invocation.md +258 -0
- package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +164 -0
- package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +94 -0
- package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +331 -0
- package/skills/service-catalog-template-search/SKILL.md +212 -0
- package/skills/service-catalog-template-search/references/cli-invocation.md +128 -0
- package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +205 -0
- package/skills/service-concierge-portal-generate/SKILL.md +126 -0
- package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1428 -0
- package/skills/service-digital-engagement-channel-configure/SKILL.md +46 -6
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +2 -1
- package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -1
- package/skills/service-helpagent-coordinate/README.md +8 -2
- package/skills/service-helpagent-coordinate/SKILL.md +126 -130
- package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +70 -53
- package/skills/service-helpagent-coordinate/references/agent-script.md +571 -457
- package/skills/service-helpagent-coordinate/references/channel-voice.md +38 -9
- package/skills/service-helpagent-coordinate/references/channel-web-chat.md +173 -49
- package/skills/service-helpagent-coordinate/references/output-report-format.md +126 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +153 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +79 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +35 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +271 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +265 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +220 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-final-report.mjs +102 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/record-enable-result.mjs +73 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +206 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +194 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +223 -0
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +45 -7
- package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +45 -5
- package/skills/service-itsm-agentic-setup-configure/SKILL.md +116 -0
- package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +64 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +158 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +361 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/error-taxonomy.md +44 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +66 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +66 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/specialized-templates.md +148 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +169 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/build-create-body.mjs +116 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +185 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +168 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/create-scratch-dir.mjs +58 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +197 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +186 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +51 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +345 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/error-taxonomy.md +44 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +63 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +44 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +149 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +110 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +201 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-activate-result.mjs +135 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +194 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +158 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/create-scratch-dir.mjs +58 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +191 -0
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +133 -0
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +71 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +308 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +23 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +193 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +57 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +394 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +266 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +106 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +142 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +82 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-action-surface.mjs +137 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-assignment-state.mjs +99 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +120 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/resolve-target-user.mjs +86 -0
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +284 -0
- package/skills/service-itsm-agentic-setup-uel-user-create/references/mcp-invocation.md +302 -0
- package/skills/service-itsm-channels-coordinate/SKILL.md +472 -0
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +212 -0
- package/skills/service-itsm-incident-mgmt-configure/references/mcp-invocation.md +225 -0
- package/skills/service-itsm-incident-priority-configure/SKILL.md +53 -12
- package/skills/service-itsm-swarming-configure/SKILL.md +212 -0
- package/skills/service-itsm-teams-configure/SKILL.md +395 -0
- package/skills/service-itsm-teams-configure/references/azure-credential-population.md +213 -0
- package/skills/service-itsm-teams-configure/references/gotchas.md +23 -0
- package/skills/service-itsm-teams-coordinate/SKILL.md +175 -0
- package/skills/service-itsm-teams-coordinate/examples/output-templates.md +85 -0
- package/skills/service-itsm-teams-debug/SKILL.md +144 -0
- package/skills/service-itsm-teams-debug/references/configuration-checklists.md +277 -0
- package/skills/service-itsm-teams-debug/references/report-generation.md +95 -0
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +139 -0
- package/skills/service-itsm-teams-employee-agent-configure/assets/Teams_AgentForce.EmbeddedServiceConfig-meta.xml +43 -0
- package/skills/service-itsm-teams-employee-agent-configure/references/teams-embedded-employee-agent.md +480 -0
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +232 -0
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +391 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Data Mask API Surface Reference
|
|
2
|
+
|
|
3
|
+
The defining characteristic of Data Mask automation is that its entities live on **different API
|
|
4
|
+
surfaces**. Reaching for the wrong one is the primary cause of failed/stuck runs. This file is the
|
|
5
|
+
authoritative map.
|
|
6
|
+
|
|
7
|
+
## Per-entity surface
|
|
8
|
+
|
|
9
|
+
| Entity | Role | Surface | Reachable by |
|
|
10
|
+
|--------|------|---------|--------------|
|
|
11
|
+
| `DataMaskPolicy` | The masking policy shell (config) | Tooling API / Metadata API | `sf data query --use-tooling-api`, MDAPI deploy (thin shell only) |
|
|
12
|
+
| `DataMaskPolicyObject` | Object targeted by a policy (+ its optional row filter) | **Tooling API only** | Tooling query + insert |
|
|
13
|
+
| `DataMaskPolicyField` | Field + masking treatment | **Tooling API only** | Tooling query + insert |
|
|
14
|
+
| `DataMaskPolicyJobRun` | The job (a single run) | **Standard data API** | `sf data query` (plain SOQL) |
|
|
15
|
+
| `DataMaskPolicyJobRunDtl` | Per-object job detail (child) | **Standard data API** | `sf data query` (plain SOQL) |
|
|
16
|
+
| `DataMaskCustomValueLibrary` | Custom replacement-value library | **Standard data API** | `sf data query` (plain SOQL) |
|
|
17
|
+
|
|
18
|
+
`DataMaskPolicyJobRunDtl` is a child of `DataMaskPolicyJobRun` via the lookup
|
|
19
|
+
**`DataMaskPolicyJobRunId`**. `DataMaskPolicy` Ids carry the **`8dm`** key prefix.
|
|
20
|
+
|
|
21
|
+
> **`DataMaskPolicy` is a THIN Metadata API type.** `sf org list metadata-types` returns
|
|
22
|
+
> `DataMaskPolicy` (directory `dataMaskPolicies`) with **no child components** (`childXmlNames: []`).
|
|
23
|
+
> Only `<label>`, `<description>`, and `<runOnRefresh>` serialize into its metadata — object and
|
|
24
|
+
> field membership is **NOT** part of the Metadata API shape (there is no inline `<policyObjects>` /
|
|
25
|
+
> `<policyFields>`, and no standalone `DataMaskPolicyObject` / `DataMaskPolicyField` Metadata API
|
|
26
|
+
> type). Membership lives entirely in the Tooling entities `DataMaskPolicyObject` and
|
|
27
|
+
> `DataMaskPolicyField`, which you both **query and insert**. So authoring a complete policy is a
|
|
28
|
+
> **two-step** operation: (1) Metadata-deploy the thin shell (in **mdapi format** — `--metadata-dir`
|
|
29
|
+
> + `package.xml`; a source-format `--source-dir` deploy fails "Could not infer a metadata type"),
|
|
30
|
+
> then (2) Tooling-insert the object + field rows against it. The Metadata deploy must come first:
|
|
31
|
+
> it creates the policy with an active revision, without which the Tooling child insert fails
|
|
32
|
+
> `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. See `policy-authoring.md` for the full recipe.
|
|
33
|
+
> `DataMaskPolicyField` treatment columns are `MaskingCategory` (`library` / `replaceRandom`) +
|
|
34
|
+
> `MaskValue` (a snake_case library token) — there is **no `MaskingRuleType` column**.
|
|
35
|
+
|
|
36
|
+
> **Schedule fields live ON the policy** — there is no separate schedule entity. `RunFrequency`
|
|
37
|
+
> (`once` / `daily` / `weekly` / `monthly`), `ScheduledStart`, and `RunOnRefresh` are fields on
|
|
38
|
+
> `DataMaskPolicy` (confirmed via Tooling describe), so scheduling folds into policy create/update.
|
|
39
|
+
|
|
40
|
+
> **Row-subset ("sample") filtering lives ON `DataMaskPolicyObject`, NOT on the policy.** There is
|
|
41
|
+
> **no `sampleSize` field** anywhere on `DataMaskPolicy`, and **no `LIMIT`** — Data Mask has no
|
|
42
|
+
> row-cap concept. To mask only a subset, set `FilterEnabled = true` and a **selective** predicate
|
|
43
|
+
> that genuinely matches fewer rows in `WhereCriteria` (a **40-char** SOQL-style predicate, e.g.
|
|
44
|
+
> `LastName LIKE '%son%'`); `RawFilterData` holds the structured form the engine actually executes.
|
|
45
|
+
> The masked count equals the number of rows the predicate matches. A `LIMIT` is **silently
|
|
46
|
+
> ignored** (`LastName != 'X' LIMIT 20` masks the whole table — 407 rows — because the predicate is
|
|
47
|
+
> always-true). Confirmed live. See `policy-authoring.md` → "Masking only a subset".
|
|
48
|
+
> **`RawFilterData`'s `operation` must be one of** `eq`, `ne`, `lt`, `gt`, `ge`, `le`, `contains`,
|
|
49
|
+
> `not_contains`, `in`, `not_in` — anything else (e.g. `startsWith`) fails the run with a `422`.
|
|
50
|
+
> **The predicate must also be valid SOQL** — Data Mask runs a planning `SELECT count() ... WHERE
|
|
51
|
+
> <predicate>`, so `Id != 'null'` fails the whole job (`invalid ID field: null`). Filter on a text
|
|
52
|
+
> field like `LastName`, not `Id`.
|
|
53
|
+
|
|
54
|
+
### What fails, and why
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# WRONG — these are Tooling/MDAPI entities, not standard-data-API objects:
|
|
58
|
+
sf sobject describe --sobject DataMaskPolicy --target-org <alias> # -> NOT_FOUND
|
|
59
|
+
sf data query --query "SELECT Id FROM DataMaskPolicy" --target-org <alias> # -> INVALID_TYPE
|
|
60
|
+
|
|
61
|
+
# RIGHT — policy config via Tooling API:
|
|
62
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
63
|
+
--query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
|
|
64
|
+
|
|
65
|
+
# RIGHT — job + job-detail via standard API:
|
|
66
|
+
sf data query --target-org <alias> \
|
|
67
|
+
--query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
|
|
68
|
+
sf data query --target-org <alias> \
|
|
69
|
+
--query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Status picklist values (`DataMaskPolicyJobRun.Status`)
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
pending, scheduled, running, completed, completed_with_errors, failed, canceled
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **Mid-run (not terminal):** `pending`, `scheduled`, `running`
|
|
79
|
+
- **Terminal (success/failure):** `completed`, `completed_with_errors`, `failed`
|
|
80
|
+
- **Terminal after abort:** `canceled` ← note the single "l"
|
|
81
|
+
|
|
82
|
+
`DataMaskPolicyJobRun.Type` picklist: `auto`, `manual`, `scheduled`.
|
|
83
|
+
|
|
84
|
+
Never report a mid-run value as the final status — poll until a terminal value appears.
|
|
85
|
+
|
|
86
|
+
> **Case differs by surface.** These SOQL picklist values are **lowercase**. The run/abort REST
|
|
87
|
+
> responses return the same states **UPPERCASE** (`RUNNING`, `CANCELED`). Poll for terminal state
|
|
88
|
+
> against the lowercase SOQL value — do not compare it to the run-API response string.
|
|
89
|
+
|
|
90
|
+
## Run / abort REST endpoints
|
|
91
|
+
|
|
92
|
+
Base: `/services/data/v67.0/platform/data-resilience/data-mask`
|
|
93
|
+
|
|
94
|
+
- Version must be **`v67.0` or later** (Core release 262, where these endpoints were added).
|
|
95
|
+
- There is **no `/connect/` segment** — the path is `/services/data/v67.0/platform/...`. A `connect`
|
|
96
|
+
segment or a pre-v67 version returns `NOT_FOUND`. (This was the top failure mode — verified live.)
|
|
97
|
+
- The id is bound as a **path segment**, not a body field.
|
|
98
|
+
|
|
99
|
+
### Start a run
|
|
100
|
+
```text
|
|
101
|
+
POST /services/data/v67.0/platform/data-resilience/data-mask/policies/{policyId}/run
|
|
102
|
+
```
|
|
103
|
+
- Empty JSON body `{}` (`sf api request rest` requires `--body` on a POST; the API takes no payload).
|
|
104
|
+
- **`200`** → accepted; response `{ jobRunId, policyId, status: "RUNNING", message: "Job started successfully" }` (status UPPERCASE).
|
|
105
|
+
- `403` → org is production (Data Mask runs are sandbox-only; runtime sandbox guard).
|
|
106
|
+
- `409`/`CONFLICT` → a run is already in progress for that policy.
|
|
107
|
+
|
|
108
|
+
### Abort a run
|
|
109
|
+
```text
|
|
110
|
+
POST /services/data/v67.0/platform/data-resilience/data-mask/jobs/{jobRunId}/abort
|
|
111
|
+
```
|
|
112
|
+
- Empty JSON body `{}`.
|
|
113
|
+
- **`200`** → abort accepted, response `status: "CANCELED"`, `message: "Job abort requested"` (async — confirm with a SOQL re-query).
|
|
114
|
+
- `404` → unknown job run id.
|
|
115
|
+
- `409` → job is not in a `running` state (already terminal or still `scheduled`).
|
|
116
|
+
- After a successful abort, `DataMaskPolicyJobRun.Status` (SOQL, lowercase) becomes `canceled`.
|
|
117
|
+
|
|
118
|
+
### Calling the run API from the CLI
|
|
119
|
+
```bash
|
|
120
|
+
# Write an empty JSON object to a file, then pass it with an @ prefix:
|
|
121
|
+
printf '{}' > ./empty-body.json
|
|
122
|
+
sf api request rest \
|
|
123
|
+
"/services/data/v67.0/platform/data-resilience/data-mask/policies/<policyId>/run" \
|
|
124
|
+
--method POST --body @./empty-body.json --target-org <alias>
|
|
125
|
+
```
|
|
126
|
+
`sf api request rest` handles auth/session automatically — no need to extract a token by hand.
|
|
127
|
+
**A file body needs the `@` prefix** (`--body @./empty-body.json`) — without it the literal path
|
|
128
|
+
string is sent as the body and the API returns `JSON_PARSER_ERROR`. The file just needs `{}`.
|
|
129
|
+
If you must call it from Apex (`HttpRequest`), use `URL.getOrgDomainURL()` +
|
|
130
|
+
`UserInfo.getSessionId()` for the base URL and bearer token.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Authoring a DataMaskPolicy
|
|
2
|
+
|
|
3
|
+
`DataMaskPolicy` is a **thin Metadata API shell**. Only three elements serialize into its
|
|
4
|
+
metadata — `<label>`, `<description>`, `<runOnRefresh>`. Object and field membership is **NOT**
|
|
5
|
+
part of the Metadata API shape (`DataMaskPolicy` has `childXmlNames: []` — there is no inline
|
|
6
|
+
`<policyObjects>` / `<policyFields>`). Membership lives in two separate **Tooling API** entities,
|
|
7
|
+
`DataMaskPolicyObject` and `DataMaskPolicyField`, which you insert as rows.
|
|
8
|
+
|
|
9
|
+
Authoring a complete, runnable policy from scratch is therefore a **two-step** operation:
|
|
10
|
+
|
|
11
|
+
1. **Metadata API deploy** the thin `DataMaskPolicy` shell. This is what creates the policy *with
|
|
12
|
+
an active revision* — the child rows in step 2 depend on it.
|
|
13
|
+
2. **Tooling API insert** the `DataMaskPolicyObject` (one per target object) and its
|
|
14
|
+
`DataMaskPolicyField` rows (one per masked field).
|
|
15
|
+
|
|
16
|
+
> **Order matters.** If you create the parent via the Tooling API instead of a Metadata deploy,
|
|
17
|
+
> it has no active revision, and the child insert fails with
|
|
18
|
+
> `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. Always Metadata-deploy the shell first.
|
|
19
|
+
|
|
20
|
+
Reuse an existing policy when one fits; author a new one only when needed.
|
|
21
|
+
|
|
22
|
+
## Reuse first
|
|
23
|
+
```bash
|
|
24
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
25
|
+
--query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
|
|
26
|
+
```
|
|
27
|
+
If a policy already targets the object/fields you need, use its `Id` and skip authoring.
|
|
28
|
+
|
|
29
|
+
## Step 1 — Metadata-deploy the thin shell
|
|
30
|
+
|
|
31
|
+
The `.dataMaskPolicy` metadata file carries ONLY these three elements. `<masterLabel>`,
|
|
32
|
+
`<developerName>`, `<sampleSize>`, and any membership element are **rejected** — they are not part
|
|
33
|
+
of the type. (There is **no `sampleSize` field anywhere** on `DataMaskPolicy` — to mask only a
|
|
34
|
+
subset of records, use the row filter on `DataMaskPolicyObject`; see "Masking only a subset" below.)
|
|
35
|
+
|
|
36
|
+
```xml
|
|
37
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
38
|
+
<DataMaskPolicy xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
39
|
+
<label>Contact PII Mask</label>
|
|
40
|
+
<description>Masks core Contact PII in sandbox.</description>
|
|
41
|
+
<runOnRefresh>false</runOnRefresh>
|
|
42
|
+
</DataMaskPolicy>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Deploy it in **mdapi (metadata) format** with a `package.xml`. `DataMaskPolicy` has **no
|
|
46
|
+
source-format SDR registry entry**, so a `--source-dir` deploy fails with *"Could not infer a
|
|
47
|
+
metadata type"*. Lay the file out as `dataMaskPolicies/Contact_PII_Mask.dataMaskPolicy` alongside
|
|
48
|
+
a `package.xml`:
|
|
49
|
+
|
|
50
|
+
```xml
|
|
51
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
52
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
53
|
+
<types>
|
|
54
|
+
<members>Contact_PII_Mask</members>
|
|
55
|
+
<name>DataMaskPolicy</name>
|
|
56
|
+
</types>
|
|
57
|
+
<version>67.0</version>
|
|
58
|
+
</Package>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sf project deploy start --metadata-dir <mdapi-dir> --target-org <alias>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The developer name of the created policy is the metadata member name (`Contact_PII_Mask`). Resolve
|
|
66
|
+
its Id before step 2:
|
|
67
|
+
```bash
|
|
68
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
69
|
+
--query "SELECT Id FROM DataMaskPolicy WHERE DeveloperName = 'Contact_PII_Mask'"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Step 2 — Tooling-insert the object + field membership
|
|
73
|
+
|
|
74
|
+
Insert one `DataMaskPolicyObject` per target object, then one `DataMaskPolicyField` per masked
|
|
75
|
+
field against that object's Id.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# One object row
|
|
79
|
+
sf data create record --use-tooling-api --sobject DataMaskPolicyObject --target-org <alias> \
|
|
80
|
+
--values "ParentPolicyId=<policyId> ObjectReference=Contact FilterEnabled=false RunInSerialMode=false"
|
|
81
|
+
# → returns the DataMaskPolicyObject Id, use it as <objectId> below
|
|
82
|
+
|
|
83
|
+
# One field row per masked field
|
|
84
|
+
sf data create record --use-tooling-api --sobject DataMaskPolicyField --target-org <alias> \
|
|
85
|
+
--values "ParentPolicyObjectId=<objectId> FieldReference=FirstName MaskingCategory=library MaskValue=first_name"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Field treatment columns
|
|
89
|
+
|
|
90
|
+
Each `DataMaskPolicyField` carries a `MaskingCategory` plus a `MaskValue` — there is **no
|
|
91
|
+
`MaskingRuleType` column** (older docs claiming values like `RandomEmail` / `RandomPhoneNumber`
|
|
92
|
+
were wrong; that column does not exist).
|
|
93
|
+
|
|
94
|
+
- `MaskingCategory` — `library` (pick a value from the built-in library, the common case) or
|
|
95
|
+
`replaceRandom` (random replacement).
|
|
96
|
+
- `MaskValue` — a snake_case library token identifying the value set. Verified tokens include:
|
|
97
|
+
`first_name`, `last_name`, `email`, `phone`, `street`, `city`, `state`, `postal_code`,
|
|
98
|
+
`country`, `account_name`, `URL`.
|
|
99
|
+
|
|
100
|
+
## Choosing treatments
|
|
101
|
+
|
|
102
|
+
Match the `MaskValue` to the field's semantics — do **not** apply one blanket replacement:
|
|
103
|
+
|
|
104
|
+
| Field | MaskingCategory | MaskValue |
|
|
105
|
+
|-------|-----------------|-----------|
|
|
106
|
+
| FirstName | `library` | `first_name` |
|
|
107
|
+
| LastName | `library` | `last_name` |
|
|
108
|
+
| Email | `library` | `email` |
|
|
109
|
+
| Phone / MobilePhone | `library` | `phone` |
|
|
110
|
+
| MailingStreet | `library` | `street` |
|
|
111
|
+
| MailingCity | `library` | `city` |
|
|
112
|
+
| MailingState | `library` | `state` |
|
|
113
|
+
| MailingPostalCode | `library` | `postal_code` |
|
|
114
|
+
| MailingCountry | `library` | `country` |
|
|
115
|
+
|
|
116
|
+
Do not mask system fields (`Id`, `CreatedDate`, `OwnerId`, etc.).
|
|
117
|
+
|
|
118
|
+
## Editing membership
|
|
119
|
+
|
|
120
|
+
To **add** a field, insert another `DataMaskPolicyField` row. To **remove** one, delete its row:
|
|
121
|
+
```bash
|
|
122
|
+
sf data delete record --use-tooling-api --sobject DataMaskPolicyField \
|
|
123
|
+
--record-id <fieldRowId> --target-org <alias>
|
|
124
|
+
```
|
|
125
|
+
The parent `DataMaskPolicy` shell does not need to be redeployed to change membership.
|
|
126
|
+
|
|
127
|
+
## Masking only a subset of records (a "sample" run)
|
|
128
|
+
|
|
129
|
+
There is **no `sampleSize`** on `DataMaskPolicy`, and **there is no `LIMIT`** — Data Mask has no
|
|
130
|
+
row-cap concept. To process only a subset of an object's rows, you write a **selective `WHERE`
|
|
131
|
+
predicate** that genuinely matches fewer rows, on the **`DataMaskPolicyObject`** row (not on the
|
|
132
|
+
policy). The masked count equals however many rows satisfy that predicate — so the predicate itself
|
|
133
|
+
*is* the subset. Three columns control it:
|
|
134
|
+
|
|
135
|
+
| Column | Type | Purpose |
|
|
136
|
+
|--------|------|---------|
|
|
137
|
+
| `FilterEnabled` | boolean | `true` turns the row filter on (default `false` = mask the whole object) |
|
|
138
|
+
| `WhereCriteria` | string, **max 40 chars** | A SOQL-style predicate — the human-readable form, e.g. `LastName LIKE '%son%'` |
|
|
139
|
+
| `RawFilterData` | textarea (JSON) | The **structured** form the engine actually executes (see the op list below) |
|
|
140
|
+
|
|
141
|
+
> **The engine masks exactly the rows the predicate matches — there is no `LIMIT`.** A `LIMIT`
|
|
142
|
+
> clause in `WhereCriteria` is silently ignored: `LastName != 'X' LIMIT 20` masked **all 407**
|
|
143
|
+
> Contacts, because `LastName != 'X'` matches every row and the `LIMIT` did nothing. To mask a
|
|
144
|
+
> subset you must pick a predicate that is **actually selective** — e.g. `LastName LIKE '%son%'`
|
|
145
|
+
> (25 rows) or `MailingState = 'NY'` (3 rows), verified with a `SELECT COUNT()` first. An
|
|
146
|
+
> always-true predicate is **not** a subset.
|
|
147
|
+
>
|
|
148
|
+
> **`RawFilterData` is what runs — its `operation` must be one of the engine's supported ops:**
|
|
149
|
+
> `eq`, `ne`, `lt`, `gt`, `ge`, `le`, `contains`, `not_contains`, `in`, `not_in`. Anything else
|
|
150
|
+
> (e.g. `startsWith`) fails the run with a `422` `literal_error`. `LIKE '%son%'` maps to
|
|
151
|
+
> `{"operation":"contains","value":"son"}`; `= 'NY'` maps to `{"operation":"eq","value":"NY"}`.
|
|
152
|
+
> `WhereCriteria` and `RawFilterData` must express the **same** predicate.
|
|
153
|
+
>
|
|
154
|
+
> **The predicate must also be valid SOQL** (Data Mask runs a planning `SELECT count() FROM <object>
|
|
155
|
+
> WHERE <predicate>` from it). **Do NOT filter on `Id`.** `Id != 'null'` fails with `invalid ID
|
|
156
|
+
> field: null` / `INVALID_QUERY_FILTER_OPERATOR` and makes the whole **job fail** with 0 records
|
|
157
|
+
> masked. Filter on a text field (`LastName`, `MailingState`, …), not `Id`.
|
|
158
|
+
|
|
159
|
+
Set them when you insert (or update) the object row. Example — mask the Contacts whose last name
|
|
160
|
+
contains `son` (a real subset; verify the count with `SELECT count() FROM Contact WHERE LastName
|
|
161
|
+
LIKE '%son%'` first). `RawFilterData` mirrors it with `operation: contains`:
|
|
162
|
+
```bash
|
|
163
|
+
sf data create record --use-tooling-api --sobject DataMaskPolicyObject --target-org <alias> \
|
|
164
|
+
--values "ParentPolicyId=<policyId> ObjectReference=Contact RunInSerialMode=false \
|
|
165
|
+
FilterEnabled=true WhereCriteria=\"LastName LIKE '%son%'\" \
|
|
166
|
+
RawFilterData={\"type\":\"and\",\"filters\":[{\"type\":\"field_filter\",\"field\":\"LastName\",\"operation\":\"contains\",\"value\":\"son\"}]}"
|
|
167
|
+
```
|
|
168
|
+
To turn an existing object row into a subset run, update it instead:
|
|
169
|
+
```bash
|
|
170
|
+
sf data update record --use-tooling-api --sobject DataMaskPolicyObject --record-id <objectId> \
|
|
171
|
+
--target-org <alias> --values "FilterEnabled=true WhereCriteria=\"LastName LIKE '%son%'\" \
|
|
172
|
+
RawFilterData={\"type\":\"and\",\"filters\":[{\"type\":\"field_filter\",\"field\":\"LastName\",\"operation\":\"contains\",\"value\":\"son\"}]}"
|
|
173
|
+
```
|
|
174
|
+
After the run, confirm the masked count in `DataMaskPolicyJobRunDtl` matches the predicate's row
|
|
175
|
+
count and is **less than** `SELECT COUNT() FROM Contact` — proving it masked a subset, not the whole
|
|
176
|
+
table. `WhereCriteria` is only **40 characters**, so keep the predicate short.
|
|
177
|
+
|
|
178
|
+
## Verifying the policy after creation
|
|
179
|
+
```bash
|
|
180
|
+
# Confirm the object + field membership landed
|
|
181
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
182
|
+
--query "SELECT Id, FieldReference, MaskingCategory, MaskValue FROM DataMaskPolicyField \
|
|
183
|
+
WHERE ParentPolicyObjectId = '<objectId>'"
|
|
184
|
+
```
|
|
185
|
+
Use the policy `Id` as `<policyId>` in the run/abort sequence (`run-and-abort.md`).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Run / Poll / Report — and Cancel
|
|
2
|
+
|
|
3
|
+
The operational commands for a Data Mask run. Assumes a policy already exists (see
|
|
4
|
+
`policy-authoring.md` to create one) and the target org is a **sandbox**.
|
|
5
|
+
|
|
6
|
+
These map to the two workflows in `SKILL.md`: **Workflow A (mask & report)** uses steps 0–4;
|
|
7
|
+
**Workflow B (cancel a run)** uses steps 5–7 and is run only when the request is to abort/cancel.
|
|
8
|
+
Pick one by the request — a mask-and-report task does not include a cancel.
|
|
9
|
+
|
|
10
|
+
Throughout, `<alias>` is the target org, `<policyId>` the `DataMaskPolicy.Id`, `<jobRunId>` the id
|
|
11
|
+
returned by the run-start call.
|
|
12
|
+
|
|
13
|
+
## 0. Confirm sandbox + context
|
|
14
|
+
```bash
|
|
15
|
+
sf org display --target-org <alias> --json
|
|
16
|
+
```
|
|
17
|
+
Check `result.isSandbox === true`. Note `result.instanceUrl`. Data Mask run endpoints return `403`
|
|
18
|
+
on production.
|
|
19
|
+
|
|
20
|
+
## 1. Identify the policy
|
|
21
|
+
```bash
|
|
22
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
23
|
+
--query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
|
|
24
|
+
```
|
|
25
|
+
Pick the policy that targets the PII you intend to mask (or create one — see `policy-authoring.md`).
|
|
26
|
+
|
|
27
|
+
## 2. Start the run (REST run API)
|
|
28
|
+
```bash
|
|
29
|
+
printf '{}' > ./empty-body.json
|
|
30
|
+
sf api request rest \
|
|
31
|
+
"/services/data/v67.0/platform/data-resilience/data-mask/policies/<policyId>/run" \
|
|
32
|
+
--method POST --body @./empty-body.json --target-org <alias>
|
|
33
|
+
```
|
|
34
|
+
`--body` must point at a file containing `{}` **with an `@` prefix** (`--body @./empty-body.json`) —
|
|
35
|
+
`sf api request rest` requires a body on POST even though this endpoint takes no payload, and without
|
|
36
|
+
the `@` the literal path string is sent as the body (→ `JSON_PARSER_ERROR`). Version must be **`v67.0`+** and there is **no `/connect/`
|
|
37
|
+
segment** (either mistake returns `NOT_FOUND`). Sample response (HTTP **200**):
|
|
38
|
+
```json
|
|
39
|
+
{ "jobRunId": "1aG...", "policyId": "8dm...", "status": "RUNNING", "message": "Job started successfully" }
|
|
40
|
+
```
|
|
41
|
+
Capture `jobRunId`. A `409`/`CONFLICT` means a run is already active for this policy.
|
|
42
|
+
|
|
43
|
+
> **Status case differs by surface.** The run API returns **UPPERCASE** status strings
|
|
44
|
+
> (`RUNNING`, `CANCELED`), while the `DataMaskPolicyJobRun.Status` SOQL picklist is **lowercase**
|
|
45
|
+
> (`running`, `canceled`). Poll for terminal state against the **SOQL** value (lowercase) — that is
|
|
46
|
+
> what `scripts/poll-job.sh` checks. `DataMaskPolicy` Ids carry the `8dm` prefix.
|
|
47
|
+
|
|
48
|
+
## 3. Poll to terminal
|
|
49
|
+
```bash
|
|
50
|
+
sf data query --target-org <alias> \
|
|
51
|
+
--query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
|
|
52
|
+
```
|
|
53
|
+
Repeat on a bounded interval until `Status` is one of `completed`, `completed_with_errors`,
|
|
54
|
+
`failed`. `scheduled`/`running` are NOT terminal. `scripts/poll-job.sh` automates this with a
|
|
55
|
+
timeout.
|
|
56
|
+
|
|
57
|
+
## 4. Report from the detail object
|
|
58
|
+
Per-object masked results live on the child, not the parent:
|
|
59
|
+
```bash
|
|
60
|
+
sf data query --target-org <alias> \
|
|
61
|
+
--query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
|
|
62
|
+
```
|
|
63
|
+
Report the concrete masked-record count and per-object success/failure from these rows. Do not
|
|
64
|
+
report a number you did not read from here.
|
|
65
|
+
|
|
66
|
+
## 5. Abort the currently-running job — ONLY when the user asked to cancel
|
|
67
|
+
Steps 5–7 are the **abort** flow. Run them **only when the prompt explicitly asks to cancel/abort**
|
|
68
|
+
a run — a "create and run" or "edit and run" task is complete after step 4.
|
|
69
|
+
|
|
70
|
+
Aborting is an **on-demand action against a job that is already in progress**: take that job's
|
|
71
|
+
`jobRunId` (and note its `DataMaskPolicyId` — you need it to start a replacement run if the window is
|
|
72
|
+
missed), wait for `DataMaskPolicyJobRun.Status = running`, and go straight to the abort (step 6).
|
|
73
|
+
You can only abort a running job. Do **not** use the terminal poll from step 3 here — it waits until
|
|
74
|
+
the job is *done*. Use the poller's `running` mode, which returns the instant the job is abortable:
|
|
75
|
+
```bash
|
|
76
|
+
POLL_MODE=running bash scripts/poll-job.sh <alias> <jobRunId> 900 15
|
|
77
|
+
```
|
|
78
|
+
- Exit `0` (prints `running`) → abort now (step 6).
|
|
79
|
+
- Exit `3` → the job reached a terminal state before `running` was caught; the window is gone. Start
|
|
80
|
+
a fresh run (step 2, using the policy Id you noted) and poll the new `jobRunId`.
|
|
81
|
+
- Exit `1` (timeout) → re-query the job. If still non-terminal, re-run the poller once more; if
|
|
82
|
+
`running`, abort; if terminal, treat as exit 3 (fresh run + poll).
|
|
83
|
+
|
|
84
|
+
**Only if no job is currently running** (the one you were watching already finished, or you're
|
|
85
|
+
reproducing the run→abort flow end-to-end) start a fresh run (repeat step 2) to have a live job to
|
|
86
|
+
cancel, poll (`POLL_MODE=running`) until `Status = running`, then abort — targeting that live run,
|
|
87
|
+
never an older already-terminal one.
|
|
88
|
+
|
|
89
|
+
## 6. Abort (REST run API)
|
|
90
|
+
```bash
|
|
91
|
+
sf api request rest \
|
|
92
|
+
"/services/data/v67.0/platform/data-resilience/data-mask/jobs/<jobRunId>/abort" \
|
|
93
|
+
--method POST --body @./empty-body.json --target-org <alias>
|
|
94
|
+
```
|
|
95
|
+
- `200` → accepted (response `status: "CANCELED"`, `message: "Job abort requested"`). `409` → not in `running` state.
|
|
96
|
+
- Do **not** abort by deleting/updating the `DataMaskPolicyJobRun` row via DML — that is not a real
|
|
97
|
+
cancellation.
|
|
98
|
+
|
|
99
|
+
## 7. Confirm the abort
|
|
100
|
+
Cancellation is asynchronous. Re-query until the status settles:
|
|
101
|
+
```bash
|
|
102
|
+
sf data query --target-org <alias> \
|
|
103
|
+
--query "SELECT Id, Status FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
|
|
104
|
+
```
|
|
105
|
+
Only report the abort as successful once `Status = canceled`.
|
|
106
|
+
|
|
107
|
+
## Failure decision table
|
|
108
|
+
|
|
109
|
+
| Symptom | Meaning | Action |
|
|
110
|
+
|---------|---------|--------|
|
|
111
|
+
| run-start `403` | Production org | Data Mask is sandbox-only; switch to a sandbox |
|
|
112
|
+
| run-start `409` | Run already active | Poll the existing run or wait |
|
|
113
|
+
| abort `409` | Job not `running` | Re-check status; may already be terminal or still `scheduled` |
|
|
114
|
+
| abort `200`, SOQL status still `running` | Async cancel in flight | Keep polling until `canceled` |
|
|
115
|
+
| run/abort `NOT_FOUND` | Wrong path — `/connect/` segment present or version < v67 | Use `/services/data/v67.0/platform/data-resilience/data-mask/...` (no `connect`) |
|
|
116
|
+
| status stuck `scheduled` | Job hasn't picked up yet | Keep polling within the cap; not an error yet |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# poll-job.sh — poll a DataMaskPolicyJobRun, with a bounded timeout. Two modes:
|
|
3
|
+
# terminal (default) — wait until the job REACHES a terminal state (used by Workflow A, run→report)
|
|
4
|
+
# running — wait until the job ENTERS the `running` state (used by Workflow B, abort)
|
|
5
|
+
#
|
|
6
|
+
# DataMaskPolicyJobRun is a STANDARD data-API object (unlike DataMaskPolicy, which is
|
|
7
|
+
# Tooling/MDAPI), so plain `sf data query` works here.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds] # terminal mode (default)
|
|
11
|
+
# POLL_MODE=running poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds]
|
|
12
|
+
# Defaults: maxSeconds=600, intervalSeconds=20
|
|
13
|
+
#
|
|
14
|
+
# WHY THESE DEFAULTS: Data Mask jobs run on a backend pool/scheduler with a ~5-10 minute floor —
|
|
15
|
+
# even a tiny (20-row) job typically does not reach a terminal state or emit detail rows for several
|
|
16
|
+
# minutes after the run starts. A short cap (e.g. 180s) gives up BEFORE the job can possibly finish,
|
|
17
|
+
# so the default cap is 600s (10 min) with a 20s interval to keep the query volume low. Do NOT poll
|
|
18
|
+
# on a tight (sub-10s) interval — it just burns tool calls against a job that cannot finish sooner.
|
|
19
|
+
#
|
|
20
|
+
# TERMINAL MODE: The parent DataMaskPolicyJobRun.Status can LAG the real job state (it may read
|
|
21
|
+
# `pending`/`running` for a while after masking finished). So this poller ALSO checks
|
|
22
|
+
# DataMaskPolicyJobRunDtl: once a `total_records_masked` detail row exists, the job is effectively
|
|
23
|
+
# done and we stop, even if the parent status hasn't caught up. Ground truth is the detail rows.
|
|
24
|
+
#
|
|
25
|
+
# RUNNING MODE (for abort): exits the instant the parent status reads `running` — the only state in
|
|
26
|
+
# which the abort endpoint accepts the request (a `pending`/`scheduled` job 409s). Because the pool
|
|
27
|
+
# floor delays the `running` window by minutes, poll with a modest interval (e.g. 15s). If the job
|
|
28
|
+
# races past `running` straight to a terminal state before we catch it, that window is gone and the
|
|
29
|
+
# abort would be pointless — the poller reports the terminal status and exits 3 so the caller can
|
|
30
|
+
# start a fresh run rather than abort a job that already finished.
|
|
31
|
+
#
|
|
32
|
+
# Exit codes:
|
|
33
|
+
# terminal mode: 0 = terminal status OR masked-count detail row observed (signal on stdout);
|
|
34
|
+
# 1 = timed out; 2 = bad args.
|
|
35
|
+
# running mode: 0 = `running` observed (prints `running`); 1 = timed out; 2 = bad args;
|
|
36
|
+
# 3 = job reached a terminal state before `running` was seen (prints that status).
|
|
37
|
+
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
ORG="${1:-}"
|
|
41
|
+
JOB_ID="${2:-}"
|
|
42
|
+
MAX_SECONDS="${3:-600}"
|
|
43
|
+
INTERVAL="${4:-20}"
|
|
44
|
+
POLL_MODE="${POLL_MODE:-terminal}"
|
|
45
|
+
|
|
46
|
+
if [[ -z "$ORG" || -z "$JOB_ID" ]]; then
|
|
47
|
+
echo "usage: [POLL_MODE=running] poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds]" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
case "$POLL_MODE" in
|
|
52
|
+
terminal|running) ;;
|
|
53
|
+
*) echo "poll-job: POLL_MODE must be 'terminal' or 'running' (got '${POLL_MODE}')" >&2; exit 2 ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
# Terminal statuses (incl. canceled after an abort). scheduled/running/pending are NOT terminal.
|
|
57
|
+
is_terminal() {
|
|
58
|
+
case "$1" in
|
|
59
|
+
completed|completed_with_errors|failed|canceled) return 0 ;;
|
|
60
|
+
*) return 1 ;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
elapsed=0
|
|
65
|
+
while (( elapsed < MAX_SECONDS )); do
|
|
66
|
+
status="$(
|
|
67
|
+
sf data query --target-org "$ORG" \
|
|
68
|
+
--query "SELECT Status FROM DataMaskPolicyJobRun WHERE Id = '${JOB_ID}'" \
|
|
69
|
+
--json 2>/dev/null \
|
|
70
|
+
| python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Status'] if r else 'UNKNOWN')"
|
|
71
|
+
)"
|
|
72
|
+
echo "[poll-job] ${JOB_ID} status=${status} (${elapsed}s/${MAX_SECONDS}s) mode=${POLL_MODE}"
|
|
73
|
+
|
|
74
|
+
if [[ "$POLL_MODE" == "running" ]]; then
|
|
75
|
+
# Abort window: stop the instant the job is `running` (the only abortable state).
|
|
76
|
+
if [[ "$status" == "running" ]]; then
|
|
77
|
+
echo "running"
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
# If it slipped past `running` to a terminal state, the abort window is gone — signal the caller.
|
|
81
|
+
if is_terminal "$status"; then
|
|
82
|
+
echo "[poll-job] job reached terminal '${status}' before 'running' was observed — abort window missed" >&2
|
|
83
|
+
echo "$status"
|
|
84
|
+
exit 3
|
|
85
|
+
fi
|
|
86
|
+
else
|
|
87
|
+
if is_terminal "$status"; then
|
|
88
|
+
echo "$status"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Ground-truth check: a masked-count detail row means the job finished even if the parent lags.
|
|
93
|
+
masked="$(
|
|
94
|
+
sf data query --target-org "$ORG" \
|
|
95
|
+
--query "SELECT Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '${JOB_ID}' AND Subtype = 'total_records_masked'" \
|
|
96
|
+
--json 2>/dev/null \
|
|
97
|
+
| python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Value'] if r else '')"
|
|
98
|
+
)"
|
|
99
|
+
if [[ -n "$masked" ]]; then
|
|
100
|
+
echo "[poll-job] detail row total_records_masked=${masked} — job effectively complete (parent status=${status})"
|
|
101
|
+
echo "completed"
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
sleep "$INTERVAL"
|
|
107
|
+
elapsed=$(( elapsed + INTERVAL ))
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
if [[ "$POLL_MODE" == "running" ]]; then
|
|
111
|
+
echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — job never reached 'running'" >&2
|
|
112
|
+
else
|
|
113
|
+
echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — neither terminal status nor masked-count detail row observed" >&2
|
|
114
|
+
fi
|
|
115
|
+
exit 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-dataspace-access-configure
|
|
3
|
-
description: "Use this skill to configure Salesforce Data Cloud DataSpace access for permission sets. Grants dataspace-level access via MDAPI PermissionSet XML with dataspaceScopes elements,
|
|
3
|
+
description: "Use this skill to configure or inspect Salesforce Data Cloud DataSpace access for permission sets. Grants dataspace-level access via MDAPI PermissionSet XML with dataspaceScopes elements, optionally grants object-level access to DMO, DLO, or CIO objects via the Object Access Grants Connect API, and inspects existing scopes via read-only PermissionSet metadata retrieval. TRIGGER when: user needs to create or update a permission set with DataSpace access, grant access to a specific dataspace, list permission sets with access to a DataSpace, configure dataAccessLevel/objectAccessLevel, add RBAC object access grants, or list/remove object access grants for a permission set + DataSpace pair. DO NOT TRIGGER when: the task is a generic permission set without dataspace access (use platform-permission-set-generate), the request is about data ingestion/streams (use data360-prepare), or the work involves creating dataspaces themselves rather than granting access to them."
|
|
4
4
|
metadata:
|
|
5
5
|
version: "1.0"
|
|
6
6
|
domains: ["Platform", "Data 360"]
|
|
@@ -8,6 +8,8 @@ metadata:
|
|
|
8
8
|
cliTools:
|
|
9
9
|
- tool: ["jq"]
|
|
10
10
|
semver: ">=1.6.0"
|
|
11
|
+
- tool: ["python3"]
|
|
12
|
+
semver: ">=3.6"
|
|
11
13
|
- tool: ["sf"]
|
|
12
14
|
semver: ">=2.0.0"
|
|
13
15
|
---
|
|
@@ -32,6 +34,7 @@ Pick exactly one case from the table below before writing any files. Each case h
|
|
|
32
34
|
| **A. Create new permset with DS access** | "create a permission set called X with dataspace scope Y" | does NOT exist yet | `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` |
|
|
33
35
|
| **B. Add DS access to existing permset** | "grant existing permission set X access to dataspace Y" | already deployed (may contain other permissions) | patched `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` — see Case B workflow below |
|
|
34
36
|
| **C. Object-level grant only** | "grant permset X access to object Z (in dataspace Y)" — permset + scope already configured | already deployed with `dataspaceScopes` | `api-request.json` (Connect API body). NO permission set XML, NO `package.xml` |
|
|
37
|
+
| **D. Inspect existing DS access** | "which permission sets have access to dataspace Y?" | any | chat/report only. NO deployable files, NO runtime API mutation |
|
|
35
38
|
|
|
36
39
|
Only emit the files listed for the case you picked. Emitting Case A/B files for a Case C prompt (or vice versa) is a correctness failure — extra files change the deployment shape.
|
|
37
40
|
|
|
@@ -44,7 +47,7 @@ Only emit the files listed for the case you picked. Emitting Case A/B files for
|
|
|
44
47
|
sf project retrieve start --metadata PermissionSet:<Name> --target-org <alias>
|
|
45
48
|
```
|
|
46
49
|
2. Open the retrieved `permissionsets/<Name>.permissionset-meta.xml`. Keep every element already there.
|
|
47
|
-
3. Insert the `<dataspaceScopes>` block for the target DataSpace (element order in the file does not matter for MDAPI). If the file already has a `<dataspaceScopes>` block **for this same DataSpace**, replace only that block. Leave every `<dataspaceScopes>` block for other DataSpaces untouched — one block per DataSpace, and
|
|
50
|
+
3. Insert the `<dataspaceScopes>` block for the target DataSpace (element order in the file does not matter for MDAPI). If the file already has a `<dataspaceScopes>` block **for this same DataSpace**, replace only that block. Leave every `<dataspaceScopes>` block for other DataSpaces untouched — one block per DataSpace. For a requested scope removal, remove only the matching block and deploy; omitting the block revokes that DataSpace grant. Verify by retrieving the PermissionSet and confirming the matching `<dataspaceScopes>` block is absent.
|
|
48
51
|
4. Write `package.xml` listing the permset in `<members>`.
|
|
49
52
|
5. Redeploy with `sf project deploy start`.
|
|
50
53
|
|
|
@@ -56,6 +59,7 @@ Trigger this skill when the user wants to:
|
|
|
56
59
|
- Create a permission set that grants access to a Data Cloud DataSpace
|
|
57
60
|
- Add or modify `dataspaceScopes` on an existing permission set
|
|
58
61
|
- Grant a permission set access to specific DMO / DLO / CIO objects in a DataSpace
|
|
62
|
+
- List which permission sets have a DataSpace scope and inspect its access levels
|
|
59
63
|
- Configure `dataAccessLevel` and `objectAccessLevel` for a DataSpace scope
|
|
60
64
|
- List or remove object access grants for a permission set + DataSpace pair
|
|
61
65
|
|
|
@@ -131,6 +135,50 @@ sf project deploy start --source-dir force-app/main/default/permissionsets/ --ta
|
|
|
131
135
|
|
|
132
136
|
---
|
|
133
137
|
|
|
138
|
+
## Read-Only DataSpace Scope Inspection — Case D
|
|
139
|
+
|
|
140
|
+
Use Case D when the user asks which permission sets have access to a DataSpace,
|
|
141
|
+
or asks to inspect `dataAccessLevel` and `objectAccessLevel` without making a
|
|
142
|
+
change.
|
|
143
|
+
|
|
144
|
+
**Never query `DataspaceScope` or `DataspaceScopeAccess` with SOQL.** Those
|
|
145
|
+
objects are not a supported query surface for this relationship. Do not try
|
|
146
|
+
SOQL as discovery, fallback, or troubleshooting.
|
|
147
|
+
|
|
148
|
+
1. Retrieve PermissionSet metadata into an isolated temporary local project and
|
|
149
|
+
output directory. Do not retrieve into the user's existing metadata tree. Save
|
|
150
|
+
the skill repository path first (as an absolute path if possible):
|
|
151
|
+
```bash
|
|
152
|
+
REPO_ROOT="<absolute/path/to/sf-skills-internal>" # or $(pwd) if you're in the repo root
|
|
153
|
+
WORK_DIR=$(mktemp -d)
|
|
154
|
+
sf project generate --name dataspace-scope-inspection --output-dir "$WORK_DIR"
|
|
155
|
+
cd "$WORK_DIR/dataspace-scope-inspection"
|
|
156
|
+
sf project retrieve start --json \
|
|
157
|
+
--metadata "PermissionSet:*" --target-org <alias> \
|
|
158
|
+
--output-dir "$WORK_DIR/retrieved"
|
|
159
|
+
```
|
|
160
|
+
Inspect the JSON result before continuing. A nonzero command status, a failed
|
|
161
|
+
`result.status`, warnings, or an unexpectedly low `result.fileProperties`
|
|
162
|
+
count means the retrieve may be incomplete. Report that limitation rather
|
|
163
|
+
than treating the result as empty, and do not fall back to SOQL.
|
|
164
|
+
2. Run the inspection script from the skill directory (do not rely on relative paths
|
|
165
|
+
after `cd` changed the working directory). Supply the retrieved directory and optional DataSpace name:
|
|
166
|
+
```bash
|
|
167
|
+
"$REPO_ROOT/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh" \
|
|
168
|
+
"$WORK_DIR/retrieved" "<DataSpace API name, if given>"
|
|
169
|
+
```
|
|
170
|
+
Report the output to the user, one result per line.
|
|
171
|
+
|
|
172
|
+
Case D is read-only with respect to the org. Do not deploy metadata, assign a
|
|
173
|
+
permission set, execute Apex, perform record DML, or make a POST, PUT, PATCH, or
|
|
174
|
+
DELETE request. Do not generate `package.xml`, PermissionSet XML, or
|
|
175
|
+
`api-request.json` in the user's workspace as part of inspection. Remove the
|
|
176
|
+
temporary work directory after reporting: `rm -rf "$WORK_DIR"`. The scope-level
|
|
177
|
+
`objectAccessLevel` is not an inventory of explicit object grants; inspect those
|
|
178
|
+
separately with the read-only object-access-grants endpoint only when requested.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
134
182
|
## Layer 2 — Object-Level Access (Connect API) — Case C
|
|
135
183
|
|
|
136
184
|
Only needed when `objectAccessLevel` is not `BY_POLICY`, or when governance policies do not cover the target objects. Grants are runtime — **no MDAPI deploy, no `package.xml`, no permission set XML**. The only artifact for a Case C task is a single `api-request.json` describing the Connect API call.
|
|
@@ -304,7 +352,7 @@ sf org api rest --target-org <alias> \
|
|
|
304
352
|
| Prefer `BY_POLICY` when data governance policies exist | Delegates row/column filtering to central policy — no per-object grants needed |
|
|
305
353
|
| One `<dataspaceScopes>` block per DataSpace | Repeat the block for multiple DataSpaces on the same permission set |
|
|
306
354
|
| Org must have Data Cloud provisioned to deploy `<dataspaceScopes>` | On non-Data-Cloud orgs, the element is ignored or rejected |
|
|
307
|
-
| Do not query `DataspaceScope` / `DataspaceScopeAccess` via SOQL | Not queryable; use
|
|
355
|
+
| Do not query `DataspaceScope` / `DataspaceScopeAccess` via SOQL | Not queryable; use Case D PermissionSet Metadata API retrieval to inspect existing scopes. Never use SOQL as a fallback. |
|
|
308
356
|
|
|
309
357
|
---
|
|
310
358
|
|