@salesforce/afv-skills 1.47.0 → 1.49.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/education-cloud-domain-configure/SKILL.md +234 -0
- package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
- package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
- package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
- package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
- package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
- package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
- package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
- package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
- package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
- package/skills/education-cloud-multi-campus-configure/SKILL.md +2 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
- package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
- package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
- package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
- package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
- package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
- package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
- package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
- package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
- package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
- package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
- package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
- package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
- package/skills/service-omni-agent-users-create/SKILL.md +96 -0
- package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
- package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
- package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
- package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
- package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
- package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
- package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
- package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
- package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
- package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
- package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
- package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
- package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
- package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
- package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
- package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
- package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
- package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
- package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
- package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
- package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
- package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
- package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
- package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
- package/skills/service-omni-queue-deploy/SKILL.md +91 -0
- package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
- package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
- package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
- package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
- package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
- package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
- package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
- package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
- package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
- package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
- package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
- package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
- package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
- package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
- package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
- package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
- package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
- package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
- package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
- package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
- package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
- package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
- package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
- package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
- package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
- package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
- package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
- package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
- package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
- package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-omni-permission-set-assign
|
|
3
|
+
description: "Assign the required Omni permission sets to provisioned agent users. TRIGGER when users ask to assign Omni permissions to agents, grant Omni-Channel access, add Omni_Agent permissions, repair missing Omni permission assignments, or prepare agents to use the Omni widget. DO NOT TRIGGER for arbitrary permission-set authoring, agent creation, or queue membership."
|
|
4
|
+
allowed-tools: Bash Read Write Edit Glob Grep
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0"
|
|
7
|
+
domains: ["Service"]
|
|
8
|
+
minApiVersion: "66.0"
|
|
9
|
+
relatedSkills:
|
|
10
|
+
- "service-omni-agent-users-create"
|
|
11
|
+
- "service-omni-presence-status-deploy"
|
|
12
|
+
- "service-omni-queue-members-assign"
|
|
13
|
+
accessCheck:
|
|
14
|
+
- type: license
|
|
15
|
+
value: ServiceCloud
|
|
16
|
+
cliTools:
|
|
17
|
+
- tool: ["jq"]
|
|
18
|
+
semver: ">=1.6"
|
|
19
|
+
- tool: ["sf"]
|
|
20
|
+
semver: ">=2.139.6"
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# service-omni-permission-set-assign
|
|
24
|
+
|
|
25
|
+
Assign one or more PermissionSets to N agent users via `PermissionSetAssignment` Data API POSTs. The default target is `Omni_Agent`, the permission set granting the OmniChannel widget, presence-status access, and demo-queue visibility — without an assignment row, agents cannot open the widget or receive routed work. Detection is SOQL-based, so the skill only creates the assignments that are missing. Agent users come from `service-omni-agent-users-create`, and it runs alongside `service-omni-presence-status-deploy` so assigned agents can both open the widget and select a status.
|
|
26
|
+
|
|
27
|
+
## Inputs
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bash scripts/verify-and-assign.sh <org-alias> [count=3] [permission-set-names-csv=Omni_Agent]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `org-alias` (required).
|
|
34
|
+
- `count` (optional, default `3`, range `1..10`) — must match the agent user count.
|
|
35
|
+
- `permission-set-names-csv` (optional, default `Omni_Agent`) — comma-separated for multiple. Every user gets every listed set (cross-product): `count=3` × 2 sets = up to 6 assignments.
|
|
36
|
+
|
|
37
|
+
## Preconditions and safety
|
|
38
|
+
|
|
39
|
+
- Target org authenticated via `sf` CLI, Service Cloud license, `sf` CLI ≥ 2.139.6.
|
|
40
|
+
- The agent users exist (`service-omni-agent-users-create`); a count mismatch blocks with a pointer back to that skill.
|
|
41
|
+
- The executing user has `PermissionsAssignPermissionSets` (standard on System Administrator) — required even for org admins; `ModifyAllData` alone is insufficient.
|
|
42
|
+
- At least one Omni presence status exists before `Omni_Agent` self-heals — the bundled set grants agent capability via `servicePresenceStatusAccesses` for whichever curated statuses exist (e.g. `Available_Case`/`Available_Voice` + `Busy`), generated at deploy time. Run `service-omni-presence-status-deploy` first; the coordinator sequences presence before permset for this reason.
|
|
43
|
+
- The three-way `safe_to_write` guard applies — assigning permission sets on a production org can escalate a real user's privileges, so it blocks with no override.
|
|
44
|
+
|
|
45
|
+
**Self-heal (run mode only).** When the default `Omni_Agent` set is absent, the skill deploys the bundled `Omni_Agent` metadata once, then assigns. In `--plan` mode it never deploys — it reports `action_needed` and exits read-only. This covers only the bundled `Omni_Agent` asset; any other permission set must already exist or the run blocks with a click-path.
|
|
46
|
+
|
|
47
|
+
## Run
|
|
48
|
+
|
|
49
|
+
`verify-and-assign.sh` performs the whole cycle:
|
|
50
|
+
|
|
51
|
+
1. Compute `safe_to_write`; derive the 8-char org suffix.
|
|
52
|
+
2. Resolve the agent users by the `agent{1..N}.<suffix>@example.com` pattern; block if any are missing.
|
|
53
|
+
3. Resolve each `PermissionSet` by name; self-heal `Omni_Agent` if absent, else block naming which is missing.
|
|
54
|
+
4. Query existing `PermissionSetAssignment` for the (user × set) cross-product; compute the missing pairs.
|
|
55
|
+
5. POST one assignment per missing pair (individual POSTs, no `allOrNone`).
|
|
56
|
+
6. Re-query to confirm final state and emit the report.
|
|
57
|
+
|
|
58
|
+
## Behavior
|
|
59
|
+
|
|
60
|
+
**Cross-product.** Every user gets every listed set; a partial assignment is a failure, not a feature.
|
|
61
|
+
|
|
62
|
+
**Idempotency.** `PermissionSetAssignment` has a database uniqueness constraint on (AssigneeId, PermissionSetId), so a re-POST raises `DUPLICATE_VALUE`; the skill detects existing pairs first and treats `DUPLICATE_VALUE` as a safety net for concurrent races. It POSTs individually so one duplicate or error never rolls back its successful siblings, and it re-queries after all POSTs — a 201 only means Salesforce accepted the write; a subsequent SOQL confirms the assignment is active.
|
|
63
|
+
|
|
64
|
+
**Non-destructive.** The skill is create-only; it never deletes existing assignments (users may hold out-of-band permissions from other admins) and derives users from the agent pattern rather than accepting an explicit user-id list, so it never assigns demo permissions to real named users.
|
|
65
|
+
|
|
66
|
+
## Output contract
|
|
67
|
+
|
|
68
|
+
A single JSON object with `status` ∈ `assigned` | `reused` | `partial` | `blocked`, the resolved `permission_sets`, `org_suffix`, `requested_count`, `expected_assignment_count` (= `requested_count × len(permission_sets)`), a `before` snapshot, `assigned_this_run`/`assigned_count`, `reused_count`, an `after` snapshot, `manual_actions`, and `blocking_issue`.
|
|
69
|
+
|
|
70
|
+
- `assigned` — at least one new assignment created; all expected pairs exist after.
|
|
71
|
+
- `reused` — all expected pairs already existed; nothing POSTed.
|
|
72
|
+
- `partial` — some POSTs failed; final count is below expected.
|
|
73
|
+
- `blocked` — precondition failed (production org, missing set, missing users, missing permissions).
|
|
74
|
+
|
|
75
|
+
`assigned_count + reused_count == expected_assignment_count` unless `partial`; `blocking_issue` is non-null only for `blocked`/`partial`.
|
|
76
|
+
|
|
77
|
+
## Limitations
|
|
78
|
+
|
|
79
|
+
- Assigns individual PermissionSets only — a `PermissionSetGroup` is a different sObject and is out of scope.
|
|
80
|
+
- Self-heal covers only the bundled `Omni_Agent`; it is not a general-purpose permission-set authoring surface.
|
|
81
|
+
- Create-only; it does not remove assignments.
|
|
82
|
+
|
|
83
|
+
## References
|
|
84
|
+
|
|
85
|
+
| File | When to read |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `assets/package.xml` | Load when the default `Omni_Agent` permission set is missing and the run-mode self-heal path must deploy the bundled metadata |
|
|
88
|
+
| `references/api-notes.md` | Before the POST loop — PermissionSetAssignment schema, its `DUPLICATE_VALUE` semantics, and why users are derived from the agent pattern rather than an explicit id list |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# API notes — PermissionSetAssignment (v66)
|
|
2
|
+
|
|
3
|
+
## Schema
|
|
4
|
+
|
|
5
|
+
`PermissionSetAssignment` is a standard sObject that links a `User` (or `Group`) to a `PermissionSet` or `PermissionSetGroup`. Every "user has permission set X" relationship is one row in this table.
|
|
6
|
+
|
|
7
|
+
Fields we care about:
|
|
8
|
+
|
|
9
|
+
| Field | Type | Purpose |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `Id` | 18-char Id (prefix `0Pa`) | The assignment row's Id; used for teardown |
|
|
12
|
+
| `AssigneeId` | Reference (`User.Id`) | The user receiving the assignment |
|
|
13
|
+
| `PermissionSetId` | Reference (`PermissionSet.Id`) | The specific PermissionSet being granted |
|
|
14
|
+
| `PermissionSetGroupId` | Reference (`PermissionSetGroup.Id`) | Alternative to PermissionSetId — for PSGroup assignments. Mutually exclusive with PermissionSetId. Not used by this skill. |
|
|
15
|
+
| `IsActive` | Boolean | Not directly writeable; managed by Salesforce |
|
|
16
|
+
| `SystemModstamp` | Datetime | Server-side write timestamp |
|
|
17
|
+
|
|
18
|
+
**We never set:**
|
|
19
|
+
- `CreatedById`, `CreatedDate`, `LastModifiedById`, `LastModifiedDate` — server-managed.
|
|
20
|
+
- `IsActive` — server-managed.
|
|
21
|
+
- `PermissionSetGroupId` — v1 skill only handles individual PermissionSets.
|
|
22
|
+
|
|
23
|
+
## Uniqueness constraint
|
|
24
|
+
|
|
25
|
+
The database has a unique index on `(AssigneeId, PermissionSetId)` — a user cannot have the same PermissionSet assigned twice. Attempting a duplicate POST raises:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
"message": "You can't add the same permission set to a user twice.",
|
|
31
|
+
"errorCode": "DUPLICATE_VALUE",
|
|
32
|
+
"fields": []
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**This is different from `GroupMember`**, where duplicate POSTs silently succeed with the existing row's Id. The DUPLICATE_VALUE handler here is reachable and correct — it treats a duplicate as "already assigned" and increments `reused_count` for that pair.
|
|
38
|
+
|
|
39
|
+
## Why we query, not upsert
|
|
40
|
+
|
|
41
|
+
`PermissionSetAssignment` has no external ID field. The Data API's upsert (`PATCH /sobjects/PermissionSetAssignment/<externalId>/<value>`) pattern is unavailable. Only:
|
|
42
|
+
|
|
43
|
+
- **POST** with `{AssigneeId, PermissionSetId}` — creates a new row (or raises DUPLICATE_VALUE).
|
|
44
|
+
- **DELETE** with `Id` — removes the assignment.
|
|
45
|
+
|
|
46
|
+
For an idempotent skill, we pre-detect existing pairs via SOQL, then POST only the missing ones — the same detect-then-POST pattern as `service-omni-queue-members-assign`.
|
|
47
|
+
|
|
48
|
+
## SOQL patterns used
|
|
49
|
+
|
|
50
|
+
**Locate the agent users** (by the same pattern `service-omni-agent-users-create` produces):
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
SELECT Id, Username
|
|
54
|
+
FROM User
|
|
55
|
+
WHERE Username IN ('agent1.<suffix>@example.com', ...)
|
|
56
|
+
```text
|
|
57
|
+
|
|
58
|
+
**Locate target permission sets:**
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
SELECT Id, Name, Label
|
|
62
|
+
FROM PermissionSet
|
|
63
|
+
WHERE Name IN ('Omni_Agent', ...)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- We use `Name` (not `Label`) — Name is the API identifier; Label is localizable.
|
|
67
|
+
- **Not `DeveloperName`** — PermissionSet does not have `DeveloperName`; `Name` IS the DeveloperName-equivalent.
|
|
68
|
+
|
|
69
|
+
**Locate existing assignments:**
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
SELECT Id, AssigneeId, PermissionSetId
|
|
73
|
+
FROM PermissionSetAssignment
|
|
74
|
+
WHERE AssigneeId IN ('005RZ...', '005RZ...', '005RZ...')
|
|
75
|
+
AND PermissionSetId IN ('0PSRZ...', '0PSRZ...')
|
|
76
|
+
```text
|
|
77
|
+
|
|
78
|
+
- Scope to specific users + specific perm sets — avoids returning hundreds of unrelated rows on production-like orgs (real users have dozens of PSAs each).
|
|
79
|
+
- The cross-product size for our default case is 3 users × 1 perm set = 3 pairs; the query returns 0..3 rows.
|
|
80
|
+
|
|
81
|
+
## POST payload
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
POST /services/data/v66.0/sobjects/PermissionSetAssignment
|
|
85
|
+
{
|
|
86
|
+
"AssigneeId": "005RZ0000005abcBBB",
|
|
87
|
+
"PermissionSetId": "0PSRZ0000067UKT4A2"
|
|
88
|
+
}
|
|
89
|
+
```text
|
|
90
|
+
|
|
91
|
+
- Success: HTTP 201 with `{"id":"0PaRZ00000ABCDE0AB","success":true,"errors":[]}`.
|
|
92
|
+
- Duplicate: HTTP 400 with `[{"errorCode":"DUPLICATE_VALUE","message":"You can't add the same permission set to a user twice.",...}]`.
|
|
93
|
+
- Missing user or perm set: HTTP 400 with `MALFORMED_ID` — indicates a skill bug (should have been caught by pre-detect).
|
|
94
|
+
- License-limit error: HTTP 400 with `LICENSE_LIMIT_EXCEEDED` — some perm sets require specific user licenses (e.g. Sales Cloud PSL requires Sales Cloud license).
|
|
95
|
+
- License-integrity error: HTTP 400 with `FIELD_INTEGRITY_EXCEPTION — The user license doesn't allow the permission: <name>` — the perm set grants a **system permission the assignee's user license does not permit**. This is why `Omni_Agent` must NOT include license-gated system permissions such as `OmniAssistiveActionUser` (an Agentforce/assistive-actions permission requiring a Service Agent/Agentforce entitlement). `Omni_Agent` instead grants agent capability via `servicePresenceStatusAccesses`, which a standard Salesforce/Service Cloud license permits. Verified live (Service Cloud profile, Salesforce license): `OmniAssistiveActionUser` → FIELD_INTEGRITY_EXCEPTION; presence-status access → assigns cleanly.
|
|
96
|
+
|
|
97
|
+
## Self-heal deploy: presence statuses are generated from what exists on the org
|
|
98
|
+
|
|
99
|
+
A `PermissionSet` deploy fails (`no ServicePresenceStatus named <name> found`) if it references a presence status that does not exist. A VoiceCall-only org has `Available_Voice` (+ `Busy`) but not `Available_Case`; a Case-only org is the reverse. To stay target-agnostic, the self-heal step does **not** deploy a static `Omni_Agent` asset with a hardcoded `Available_Case`. Instead it queries `ServicePresenceStatus` on the org, intersects with the curated Omni demo set (`Available_Case`, `Available_Voice`, `Available_Messaging`, `Available_Chat`, `Available_Incident`, `Busy`), and generates the `servicePresenceStatusAccesses` block from only the statuses that actually exist — then deploys that generated permset from a temp SFDX project. As a result, `Omni_Agent` deploys cleanly on a Case org, a Voice org, or any mix, and the deploy never references a missing status. Run `service-omni-presence-status-deploy` first so at least one presence status exists.
|
|
100
|
+
|
|
101
|
+
## sf api request rest emits a beta warning to stderr
|
|
102
|
+
|
|
103
|
+
`sf api request rest` prints `Warning: This command is currently in beta.` to stderr. To keep stdout pure JSON, the script captures stderr with the temp-file pattern:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
POST_STDERR="$(mktemp)"
|
|
107
|
+
POST_RESULT=$(sf api request rest ... 2>"$POST_STDERR" || true)
|
|
108
|
+
POST_SUCCESS=$(echo "$POST_RESULT" | jq -r '.success // false')
|
|
109
|
+
rm -f "$POST_STDERR"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Never `2>&1` — the Warning line breaks jq parsing.
|
|
113
|
+
|
|
114
|
+
## Permission required to POST PermissionSetAssignment
|
|
115
|
+
|
|
116
|
+
The user executing the sf CLI must have:
|
|
117
|
+
|
|
118
|
+
- `PermissionsAssignPermissionSets = true` (part of System Administrator profile; also included in the "Manage Permission Sets" standard permission)
|
|
119
|
+
- `ModifyAllData = true` (usually implied by the above)
|
|
120
|
+
|
|
121
|
+
If the CLI user lacks these, POST returns `INSUFFICIENT_ACCESS_OR_READONLY`. The skill's script translates this to "Re-authenticate as a System Administrator".
|
|
122
|
+
|
|
123
|
+
**Notable gotcha:** on some orgs, a user with `ModifyAllData=true` but WITHOUT `AssignPermissionSets=true` can query PSAs but not create them. This is a documented Salesforce behavior; our production CDOs typically bundle both perms on the SA profile, but scratch orgs sometimes ship the SA profile without `AssignPermissionSets`. If encountered, re-auth as a "true admin" or grant `AssignPermissionSets` manually.
|
|
124
|
+
|
|
125
|
+
## Teardown hook (not implemented in v1)
|
|
126
|
+
|
|
127
|
+
A future `service-omni-permission-set-remove` skill would:
|
|
128
|
+
|
|
129
|
+
1. Query `PermissionSetAssignment` where `AssigneeId IN <agent-users>` AND `PermissionSet.Name IN <target-names>`.
|
|
130
|
+
2. DELETE each row via `/sobjects/PermissionSetAssignment/<id>`.
|
|
131
|
+
3. Re-verify.
|
|
132
|
+
|
|
133
|
+
Not part of v1 coordinator's forward path — teardown is out of scope. A separate `teardown-coordinate` skill will handle it holistically.
|
|
134
|
+
|
|
135
|
+
## Why not Apex?
|
|
136
|
+
|
|
137
|
+
`PermissionSetAssignment` insert is a simple 2-field row. Apex would let us bulk-insert all N rows in one transaction (~1 API round-trip instead of N), but the tradeoffs are:
|
|
138
|
+
|
|
139
|
+
- **N is small.** At 3 users × 1 perm set = 3 POSTs, the difference is a couple of seconds.
|
|
140
|
+
- **Simpler debugging.** REST POSTs return structured JSON; Apex debug logs require anchored regexes.
|
|
141
|
+
- **License-check surface.** POST returns `LICENSE_LIMIT_EXCEEDED` immediately for the offending pair without rolling back sibling successes. A bulk Apex insert would need more code to detect and report a partial success.
|
|
142
|
+
|
|
143
|
+
Assigning hundreds of perm sets at once would be a reason to revisit this and use Apex with `Database.insert(..., false)` for partial-success semantics.
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# verify-and-assign.sh - assign PermissionSets to N agent users via PermissionSetAssignment POSTs; idempotent (SOQL detects existing pairs), non-destructive. Args, output contract, exit codes: SKILL.md.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 1 ]; then
|
|
7
|
+
echo '{"error":"Usage: bash verify-and-assign.sh <org-alias> [<count>] [<permission-set-names-csv>]"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
COUNT="${2:-3}"
|
|
13
|
+
PS_NAMES_CSV="${3:-Omni_Agent}"
|
|
14
|
+
API_VERSION="v66.0"
|
|
15
|
+
|
|
16
|
+
# Sanity: count 1..10
|
|
17
|
+
if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ] || [ "$COUNT" -gt 10 ]; then
|
|
18
|
+
echo "{\"error\":\"Invalid count '$COUNT' - must be integer 1..10\"}" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
|
|
23
|
+
echo "{\"error\":\"Org alias '$ORG' is not authenticated.\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Parse CSV into a JSON array
|
|
28
|
+
PS_NAMES_JSON=$(echo "$PS_NAMES_CSV" | jq -R -c 'split(",") | map(gsub("^\\s+|\\s+$"; ""))')
|
|
29
|
+
PS_COUNT=$(echo "$PS_NAMES_JSON" | jq 'length')
|
|
30
|
+
|
|
31
|
+
# Each PermissionSet Name is interpolated into SOQL, so validate its DeveloperName shape before any
|
|
32
|
+
# sf call; an empty CSV is also rejected.
|
|
33
|
+
if [ "$PS_COUNT" -lt 1 ]; then
|
|
34
|
+
echo "{\"error\":\"No permission-set names supplied.\"}" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
while IFS= read -r ps_name; do
|
|
38
|
+
if ! [[ "$ps_name" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
39
|
+
echo "{\"error\":\"Invalid permission-set name '$ps_name'. Each must start with a letter and contain only letters, digits, and underscores (max 80 chars).\"}" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
done < <(echo "$PS_NAMES_JSON" | jq -r '.[]')
|
|
43
|
+
|
|
44
|
+
# Helper: emit blocked JSON
|
|
45
|
+
emit_blocked() {
|
|
46
|
+
local msg="$1"
|
|
47
|
+
local click_path="${2:-}"
|
|
48
|
+
local suffix="${3:-}"
|
|
49
|
+
|
|
50
|
+
local manual_actions='[]'
|
|
51
|
+
if [ -n "$click_path" ]; then
|
|
52
|
+
manual_actions=$(jq -n --arg cp "$click_path" '[{id: "MANUAL_FIX", title: "Manual fix required", click_path: $cp}]')
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
jq -n \
|
|
56
|
+
--arg msg "$msg" \
|
|
57
|
+
--arg suf "$suffix" \
|
|
58
|
+
--argjson count "$COUNT" \
|
|
59
|
+
--argjson ps_count "$PS_COUNT" \
|
|
60
|
+
--argjson ma "$manual_actions" \
|
|
61
|
+
'{
|
|
62
|
+
skill: "service-omni-permission-set-assign",
|
|
63
|
+
status: "blocked",
|
|
64
|
+
permission_sets: [],
|
|
65
|
+
org_suffix: (if $suf == "" then null else $suf end),
|
|
66
|
+
requested_count: $count,
|
|
67
|
+
expected_assignment_count: ($count * $ps_count),
|
|
68
|
+
before: null,
|
|
69
|
+
assigned_this_run: [],
|
|
70
|
+
assigned_count: 0,
|
|
71
|
+
reused_count: 0,
|
|
72
|
+
after: null,
|
|
73
|
+
manual_actions: $ma,
|
|
74
|
+
blocking_issue: $msg
|
|
75
|
+
}'
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Helper: emit read-only plan preview JSON and exit 0 (PLAN_ONLY mode)
|
|
80
|
+
emit_plan() {
|
|
81
|
+
local status="$1"
|
|
82
|
+
local detail="$2"
|
|
83
|
+
jq -n \
|
|
84
|
+
--arg status "$status" \
|
|
85
|
+
--arg detail "$detail" \
|
|
86
|
+
'{
|
|
87
|
+
skill: "service-omni-permission-set-assign",
|
|
88
|
+
status: $status,
|
|
89
|
+
plan_mode: true,
|
|
90
|
+
plan_detail: $detail,
|
|
91
|
+
manual_actions: [],
|
|
92
|
+
blocking_issue: null
|
|
93
|
+
}'
|
|
94
|
+
exit 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Step 1 - Organization + safe_to_write
|
|
98
|
+
ORG_JSON=$(sf data query --target-org "$ORG" \
|
|
99
|
+
--query "SELECT Id, IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" \
|
|
100
|
+
--json 2>/dev/null)
|
|
101
|
+
|
|
102
|
+
ORG_ID=$(echo "$ORG_JSON" | jq -r '.result.records[0].Id')
|
|
103
|
+
IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
|
|
104
|
+
TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
|
|
105
|
+
ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
|
|
106
|
+
|
|
107
|
+
SAFE_TO_WRITE=false
|
|
108
|
+
if [ "$IS_SANDBOX" = "true" ] \
|
|
109
|
+
|| [ "$TRIAL_EXP" != "null" ] \
|
|
110
|
+
|| [ "$ORG_TYPE" = "Developer Edition" ] \
|
|
111
|
+
|| [ "$ORG_TYPE" = "Base Edition" ]; then
|
|
112
|
+
SAFE_TO_WRITE=true
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
SUFFIX=$(echo "$ORG_ID" | tr '[:upper:]' '[:lower:]' | tail -c 9 | head -c 8)
|
|
116
|
+
|
|
117
|
+
if [ "$SAFE_TO_WRITE" != "true" ]; then
|
|
118
|
+
emit_blocked "Refusing to assign permission sets on a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org." "" "$SUFFIX"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Step 2 - Locate target PermissionSets
|
|
122
|
+
# Build IN clause for PS names
|
|
123
|
+
PS_IN_CLAUSE=""
|
|
124
|
+
for name in $(echo "$PS_NAMES_JSON" | jq -r '.[]'); do
|
|
125
|
+
if [ -n "$PS_IN_CLAUSE" ]; then PS_IN_CLAUSE="$PS_IN_CLAUSE,"; fi
|
|
126
|
+
PS_IN_CLAUSE="$PS_IN_CLAUSE'$name'"
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
PS_JSON=$(sf data query --target-org "$ORG" \
|
|
130
|
+
--query "SELECT Id, Name, Label FROM PermissionSet WHERE Name IN ($PS_IN_CLAUSE)" \
|
|
131
|
+
--json 2>/dev/null)
|
|
132
|
+
FOUND_PS_COUNT=$(echo "$PS_JSON" | jq -r '.result.totalSize // 0')
|
|
133
|
+
|
|
134
|
+
# Self-heal: if an expected PermissionSet is missing but bundled as an asset, deploy it and re-query
|
|
135
|
+
# (so the chain works on a fresh org). safe_to_write was already enforced above.
|
|
136
|
+
if [ "$FOUND_PS_COUNT" -lt "$PS_COUNT" ]; then
|
|
137
|
+
FOUND_NAMES=$(echo "$PS_JSON" | jq -c '[.result.records[].Name]')
|
|
138
|
+
MISSING_NAMES=$(jq -n --argjson e "$PS_NAMES_JSON" --argjson f "$FOUND_NAMES" '$e - $f')
|
|
139
|
+
# PLAN_ONLY must be read-only: report the missing perm set(s) and stop before the deploy below.
|
|
140
|
+
if [ "${PLAN_ONLY:-}" = "1" ]; then
|
|
141
|
+
emit_plan "action_needed" "PermissionSet(s) $MISSING_NAMES not present. A --run would deploy the bundled asset (or require manual creation) before assigning; --plan makes no writes."
|
|
142
|
+
fi
|
|
143
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
144
|
+
ASSETS_DIR="$SCRIPT_DIR/../assets"
|
|
145
|
+
DEPLOYED_ANY=false
|
|
146
|
+
SELF_HEAL_ERR=""
|
|
147
|
+
|
|
148
|
+
# A PermissionSet deploy fails if it references a ServicePresenceStatus absent on the org, so
|
|
149
|
+
# rebuild the servicePresenceStatusAccesses block from the curated statuses that actually exist
|
|
150
|
+
# (none → deploy with no presence access, still a valid grant, rather than failing the chain).
|
|
151
|
+
build_omni_agent_permset() {
|
|
152
|
+
local out_file="$1"
|
|
153
|
+
local existing accesses="" s
|
|
154
|
+
existing=$(sf data query --target-org "$ORG" \
|
|
155
|
+
--query "SELECT DeveloperName FROM ServicePresenceStatus" --json 2>/dev/null \
|
|
156
|
+
| jq -r '[.result.records[].DeveloperName] // []' 2>/dev/null)
|
|
157
|
+
for s in Available_Case Available_Voice Available_Messaging Available_Chat Available_Incident Busy; do
|
|
158
|
+
if echo "$existing" | jq -e --arg s "$s" 'index($s)' >/dev/null 2>&1; then
|
|
159
|
+
accesses="${accesses} <servicePresenceStatusAccesses>
|
|
160
|
+
<enabled>true</enabled>
|
|
161
|
+
<servicePresenceStatus>${s}</servicePresenceStatus>
|
|
162
|
+
</servicePresenceStatusAccesses>
|
|
163
|
+
"
|
|
164
|
+
fi
|
|
165
|
+
done
|
|
166
|
+
cat > "$out_file" <<XML
|
|
167
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
168
|
+
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
169
|
+
<description>Omni-Channel agent access for demo/test orgs: grants the service presence statuses that exist on the org so agents go online and receive routed work. No ViewAllData/ModifyAllData/ManageUsers or license-gated perms.</description>
|
|
170
|
+
<hasActivationRequired>false</hasActivationRequired>
|
|
171
|
+
<label>Omni Agent</label>
|
|
172
|
+
${accesses}</PermissionSet>
|
|
173
|
+
XML
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for miss in $(echo "$MISSING_NAMES" | jq -r '.[]'); do
|
|
177
|
+
if [ -f "$ASSETS_DIR/force-app/main/default/permissionsets/$miss.permissionset-meta.xml" ]; then
|
|
178
|
+
if [ "$miss" = "Omni_Agent" ]; then
|
|
179
|
+
# Deploy a dynamically-generated Omni_Agent (never references a non-existent presence status).
|
|
180
|
+
SH_WORK="$(mktemp -d)"
|
|
181
|
+
mkdir -p "$SH_WORK/force-app/main/default/permissionsets"
|
|
182
|
+
cat > "$SH_WORK/sfdx-project.json" <<'JSON'
|
|
183
|
+
{ "packageDirectories": [{ "path": "force-app", "default": true }], "namespace": "", "sourceApiVersion": "66.0" }
|
|
184
|
+
JSON
|
|
185
|
+
build_omni_agent_permset "$SH_WORK/force-app/main/default/permissionsets/Omni_Agent.permissionset-meta.xml"
|
|
186
|
+
DEPLOY_OUT=$( cd "$SH_WORK" && sf project deploy start --target-org "$ORG" --metadata "PermissionSet:Omni_Agent" --json 2>/dev/null ) && DEPLOYED_ANY=true || \
|
|
187
|
+
SELF_HEAL_ERR="$SELF_HEAL_ERR $(echo "$DEPLOY_OUT" | jq -r '(.result.details.componentFailures // [] | map(.problem) | join("; ")) // .message // ""' 2>/dev/null)"
|
|
188
|
+
rm -rf "$SH_WORK"
|
|
189
|
+
else
|
|
190
|
+
DEPLOY_OUT=$( cd "$ASSETS_DIR" && sf project deploy start --target-org "$ORG" --metadata "PermissionSet:$miss" --json 2>/dev/null ) && DEPLOYED_ANY=true || \
|
|
191
|
+
SELF_HEAL_ERR="$SELF_HEAL_ERR $(echo "$DEPLOY_OUT" | jq -r '(.result.details.componentFailures // [] | map(.problem) | join("; ")) // .message // ""' 2>/dev/null)"
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
done
|
|
195
|
+
if [ "$DEPLOYED_ANY" = "true" ]; then
|
|
196
|
+
PS_JSON=$(sf data query --target-org "$ORG" \
|
|
197
|
+
--query "SELECT Id, Name, Label FROM PermissionSet WHERE Name IN ($PS_IN_CLAUSE)" \
|
|
198
|
+
--json 2>/dev/null)
|
|
199
|
+
FOUND_PS_COUNT=$(echo "$PS_JSON" | jq -r '.result.totalSize // 0')
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Block only if a permission set is STILL missing after the self-heal deploy attempt.
|
|
204
|
+
if [ "$FOUND_PS_COUNT" -lt "$PS_COUNT" ]; then
|
|
205
|
+
FOUND_NAMES=$(echo "$PS_JSON" | jq -c '[.result.records[].Name]')
|
|
206
|
+
MISSING_NAMES=$(jq -n --argjson expected "$PS_NAMES_JSON" --argjson found "$FOUND_NAMES" \
|
|
207
|
+
'$expected - $found')
|
|
208
|
+
emit_blocked \
|
|
209
|
+
"Expected PermissionSets $PS_NAMES_JSON; found $FOUND_NAMES. Missing: $MISSING_NAMES. Bundled perm sets deploy automatically; a non-bundled name must be created first via Setup or a package deploy.${SELF_HEAL_ERR:+ Self-heal deploy failed:${SELF_HEAL_ERR}}" \
|
|
210
|
+
"The bundled Omni_Agent grants whichever Omni service presence statuses exist on the org (Available_Case/Available_Voice/Available_Messaging/Busy). Run service-omni-presence-status-deploy first so at least one presence status exists. Otherwise: Setup → Permission Sets → New → Name: (missing DeveloperName) → configure access → Save, or deploy from your manifest: sf project deploy start -x <path-to-package.xml> -o $ORG" \
|
|
211
|
+
"$SUFFIX"
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# Build permission_sets array for the report
|
|
215
|
+
PERMISSION_SETS_JSON=$(echo "$PS_JSON" | jq -c '[.result.records[] | {developer_name: .Name, id: .Id, label: .Label}]')
|
|
216
|
+
PS_IDS_ARR=$(echo "$PS_JSON" | jq -c '[.result.records[].Id]')
|
|
217
|
+
|
|
218
|
+
# Build SOQL IN clause for PS IDs
|
|
219
|
+
PS_IDS_IN=""
|
|
220
|
+
for pid in $(echo "$PS_IDS_ARR" | jq -r '.[]'); do
|
|
221
|
+
if [ -n "$PS_IDS_IN" ]; then PS_IDS_IN="$PS_IDS_IN,"; fi
|
|
222
|
+
PS_IDS_IN="$PS_IDS_IN'$pid'"
|
|
223
|
+
done
|
|
224
|
+
|
|
225
|
+
# Step 3 - Locate expected agent users
|
|
226
|
+
EXPECTED_USERNAMES=()
|
|
227
|
+
for i in $(seq 1 "$COUNT"); do
|
|
228
|
+
EXPECTED_USERNAMES+=("agent${i}.${SUFFIX}@example.com")
|
|
229
|
+
done
|
|
230
|
+
|
|
231
|
+
USER_IN_CLAUSE=""
|
|
232
|
+
for un in "${EXPECTED_USERNAMES[@]}"; do
|
|
233
|
+
if [ -n "$USER_IN_CLAUSE" ]; then USER_IN_CLAUSE="$USER_IN_CLAUSE,"; fi
|
|
234
|
+
USER_IN_CLAUSE="$USER_IN_CLAUSE'$un'"
|
|
235
|
+
done
|
|
236
|
+
|
|
237
|
+
# Only active users are valid assignees; filter IsActive=true so an inactive occupant can't satisfy the count.
|
|
238
|
+
U_JSON=$(sf data query --target-org "$ORG" \
|
|
239
|
+
--query "SELECT Id, Username FROM User WHERE Username IN ($USER_IN_CLAUSE) AND IsActive=true" \
|
|
240
|
+
--json 2>/dev/null)
|
|
241
|
+
|
|
242
|
+
U_COUNT=$(echo "$U_JSON" | jq -r '.result.totalSize // 0')
|
|
243
|
+
if [ "$U_COUNT" -lt "$COUNT" ]; then
|
|
244
|
+
FOUND=$(echo "$U_JSON" | jq -c '[.result.records[].Username]')
|
|
245
|
+
emit_blocked \
|
|
246
|
+
"Expected $COUNT ACTIVE agent users matching pattern 'agent{1..$COUNT}.$SUFFIX@example.com'; found $U_COUNT active ($FOUND). Run service-omni-agent-users-create with count=$COUNT first (and reactivate any deactivated agent users)." \
|
|
247
|
+
"" "$SUFFIX"
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
USERS_JSON=$(echo "$U_JSON" | jq -c '[.result.records[] | {id: .Id, username: .Username}]')
|
|
251
|
+
USER_IDS_ARR=$(echo "$USERS_JSON" | jq -c '[.[].id]')
|
|
252
|
+
|
|
253
|
+
USER_IDS_IN=""
|
|
254
|
+
for uid in $(echo "$USER_IDS_ARR" | jq -r '.[]'); do
|
|
255
|
+
if [ -n "$USER_IDS_IN" ]; then USER_IDS_IN="$USER_IDS_IN,"; fi
|
|
256
|
+
USER_IDS_IN="$USER_IDS_IN'$uid'"
|
|
257
|
+
done
|
|
258
|
+
|
|
259
|
+
# Step 4 - Query existing PermissionSetAssignments for these pairs
|
|
260
|
+
PSA_JSON=$(sf data query --target-org "$ORG" \
|
|
261
|
+
--query "SELECT Id, AssigneeId, PermissionSetId FROM PermissionSetAssignment WHERE AssigneeId IN ($USER_IDS_IN) AND PermissionSetId IN ($PS_IDS_IN)" \
|
|
262
|
+
--json 2>/dev/null)
|
|
263
|
+
|
|
264
|
+
# Build the "existing_pairs" set as JSON array of {user_id, permission_set_id}
|
|
265
|
+
EXISTING_PAIRS=$(echo "$PSA_JSON" | jq -c '[.result.records[] | {user_id: .AssigneeId, permission_set_id: .PermissionSetId}]')
|
|
266
|
+
EXISTING_COUNT=$(echo "$EXISTING_PAIRS" | jq 'length')
|
|
267
|
+
|
|
268
|
+
# Build the "expected_pairs" set as cross-product of users x perm-sets
|
|
269
|
+
EXPECTED_PAIRS=$(jq -n --argjson users "$USERS_JSON" --argjson pss "$PERMISSION_SETS_JSON" \
|
|
270
|
+
'[$users[] as $u | $pss[] as $p | {user_id: $u.id, username: $u.username, permission_set_id: $p.id, permission_set_developer_name: $p.developer_name}]')
|
|
271
|
+
|
|
272
|
+
EXPECTED_COUNT=$((COUNT * PS_COUNT))
|
|
273
|
+
|
|
274
|
+
# missing = expected - existing (compared on user_id + permission_set_id keys)
|
|
275
|
+
MISSING_PAIRS=$(jq -n --argjson expected "$EXPECTED_PAIRS" --argjson existing "$EXISTING_PAIRS" \
|
|
276
|
+
'[$expected[] | . as $e | select($existing | map(select(.user_id == $e.user_id and .permission_set_id == $e.permission_set_id)) | length == 0)]')
|
|
277
|
+
MISSING_COUNT=$(echo "$MISSING_PAIRS" | jq 'length')
|
|
278
|
+
|
|
279
|
+
# PLAN_ONLY: all detection above is read-only. Report what a --run would do and stop before any write.
|
|
280
|
+
if [ "${PLAN_ONLY:-}" = "1" ]; then
|
|
281
|
+
if [ "$MISSING_COUNT" -eq 0 ]; then
|
|
282
|
+
emit_plan "reused" "All $EXPECTED_COUNT (user x permission-set) assignment(s) already present."
|
|
283
|
+
else
|
|
284
|
+
emit_plan "action_needed" "Would assign $MISSING_COUNT missing of $EXPECTED_COUNT expected (user x permission-set) pair(s)."
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# Step 5 - POST PermissionSetAssignment for each missing pair
|
|
289
|
+
ASSIGNED_THIS_RUN='[]'
|
|
290
|
+
ASSIGNED_COUNT=0
|
|
291
|
+
PARTIAL_ERRORS=()
|
|
292
|
+
|
|
293
|
+
if [ "$MISSING_COUNT" -gt 0 ]; then
|
|
294
|
+
for row in $(echo "$MISSING_PAIRS" | jq -c '.[]'); do
|
|
295
|
+
USER_ID=$(echo "$row" | jq -r '.user_id')
|
|
296
|
+
USERNAME=$(echo "$row" | jq -r '.username')
|
|
297
|
+
PS_ID=$(echo "$row" | jq -r '.permission_set_id')
|
|
298
|
+
PS_DEV_NAME=$(echo "$row" | jq -r '.permission_set_developer_name')
|
|
299
|
+
|
|
300
|
+
# Redirect the sf beta warning off stderr so stdout is pure JSON for jq.
|
|
301
|
+
POST_STDERR="$(mktemp)"
|
|
302
|
+
POST_RESULT=$(sf api request rest --target-org "$ORG" \
|
|
303
|
+
"/services/data/$API_VERSION/sobjects/PermissionSetAssignment" \
|
|
304
|
+
--method POST \
|
|
305
|
+
--body "{\"AssigneeId\":\"$USER_ID\",\"PermissionSetId\":\"$PS_ID\"}" 2>"$POST_STDERR" || true)
|
|
306
|
+
|
|
307
|
+
POST_SUCCESS=$(echo "$POST_RESULT" | jq -r '.success // false' 2>/dev/null || echo "false")
|
|
308
|
+
|
|
309
|
+
if [ "$POST_SUCCESS" = "true" ]; then
|
|
310
|
+
PSA_ID=$(echo "$POST_RESULT" | jq -r '.id')
|
|
311
|
+
ASSIGNED_THIS_RUN=$(echo "$ASSIGNED_THIS_RUN" | jq -c \
|
|
312
|
+
--arg uid "$USER_ID" --arg un "$USERNAME" \
|
|
313
|
+
--arg psdn "$PS_DEV_NAME" --arg psaid "$PSA_ID" \
|
|
314
|
+
'. + [{user_id: $uid, username: $un, permission_set_developer_name: $psdn, assignment_id: $psaid}]')
|
|
315
|
+
ASSIGNED_COUNT=$((ASSIGNED_COUNT + 1))
|
|
316
|
+
else
|
|
317
|
+
ERR_CODE=$(echo "$POST_RESULT" | jq -r 'if type == "array" then .[0].errorCode else .errorCode // "UNKNOWN" end' 2>/dev/null || echo "UNKNOWN")
|
|
318
|
+
ERR_MSG=$(echo "$POST_RESULT" | jq -r 'if type == "array" then .[0].message else .message // "Unknown POST error" end' 2>/dev/null || echo "Unknown POST error")
|
|
319
|
+
|
|
320
|
+
if [ "$ERR_CODE" = "UNKNOWN" ] && [ -z "$POST_RESULT" ]; then
|
|
321
|
+
ERR_MSG=$(grep -v '^Warning:' "$POST_STDERR" | head -1 || echo "sf CLI produced no output")
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
if [ "$ERR_CODE" = "DUPLICATE_VALUE" ]; then
|
|
325
|
+
# Concurrent race (assigned between detect and POST); treat as reused, don't increment.
|
|
326
|
+
:
|
|
327
|
+
else
|
|
328
|
+
PARTIAL_ERRORS+=("$USERNAME ($USER_ID) → $PS_DEV_NAME: $ERR_CODE - $ERR_MSG")
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
331
|
+
rm -f "$POST_STDERR"
|
|
332
|
+
done
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# Step 6 - Re-query final assignment state
|
|
336
|
+
AFTER_PSA_JSON=$(sf data query --target-org "$ORG" \
|
|
337
|
+
--query "SELECT AssigneeId, PermissionSetId FROM PermissionSetAssignment WHERE AssigneeId IN ($USER_IDS_IN) AND PermissionSetId IN ($PS_IDS_IN)" \
|
|
338
|
+
--json 2>/dev/null)
|
|
339
|
+
|
|
340
|
+
AFTER_PAIRS=$(echo "$AFTER_PSA_JSON" | jq -c '[.result.records[] | {user_id: .AssigneeId, permission_set_id: .PermissionSetId}]')
|
|
341
|
+
AFTER_COUNT=$(echo "$AFTER_PAIRS" | jq 'length')
|
|
342
|
+
|
|
343
|
+
# Step 7 - Compute status
|
|
344
|
+
REUSED_COUNT=$EXISTING_COUNT
|
|
345
|
+
|
|
346
|
+
if [ ${#PARTIAL_ERRORS[@]} -gt 0 ] || [ "$AFTER_COUNT" -lt "$EXPECTED_COUNT" ]; then
|
|
347
|
+
STATUS="partial"
|
|
348
|
+
BLOCKING_ISSUE="Partial assign: after re-query, $AFTER_COUNT of $EXPECTED_COUNT expected pairs are present. Failures: ${PARTIAL_ERRORS[*]:-"(none; count mismatch only)"}"
|
|
349
|
+
elif [ "$ASSIGNED_COUNT" -eq 0 ]; then
|
|
350
|
+
STATUS="reused"
|
|
351
|
+
BLOCKING_ISSUE=""
|
|
352
|
+
else
|
|
353
|
+
STATUS="assigned"
|
|
354
|
+
BLOCKING_ISSUE=""
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Step 8 - Emit report
|
|
358
|
+
BLOCKING_JQ_ARG='null'
|
|
359
|
+
if [ -n "$BLOCKING_ISSUE" ]; then
|
|
360
|
+
BLOCKING_JQ_ARG=$(jq -n --arg s "$BLOCKING_ISSUE" '$s')
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
jq -n \
|
|
364
|
+
--arg status "$STATUS" \
|
|
365
|
+
--argjson pss "$PERMISSION_SETS_JSON" \
|
|
366
|
+
--arg suf "$SUFFIX" \
|
|
367
|
+
--argjson count "$COUNT" \
|
|
368
|
+
--argjson expected "$EXPECTED_COUNT" \
|
|
369
|
+
--argjson before_pairs "$EXISTING_PAIRS" \
|
|
370
|
+
--argjson before_count "$EXISTING_COUNT" \
|
|
371
|
+
--argjson assigned "$ASSIGNED_THIS_RUN" \
|
|
372
|
+
--argjson assigned_count "$ASSIGNED_COUNT" \
|
|
373
|
+
--argjson reused_count "$REUSED_COUNT" \
|
|
374
|
+
--argjson after_pairs "$AFTER_PAIRS" \
|
|
375
|
+
--argjson after_count "$AFTER_COUNT" \
|
|
376
|
+
--argjson blocking "$BLOCKING_JQ_ARG" \
|
|
377
|
+
'{
|
|
378
|
+
skill: "service-omni-permission-set-assign",
|
|
379
|
+
status: $status,
|
|
380
|
+
permission_sets: $pss,
|
|
381
|
+
org_suffix: $suf,
|
|
382
|
+
requested_count: $count,
|
|
383
|
+
expected_assignment_count: $expected,
|
|
384
|
+
before: {
|
|
385
|
+
existing_assignment_pairs: $before_pairs,
|
|
386
|
+
existing_count: $before_count
|
|
387
|
+
},
|
|
388
|
+
assigned_this_run: $assigned,
|
|
389
|
+
assigned_count: $assigned_count,
|
|
390
|
+
reused_count: $reused_count,
|
|
391
|
+
after: {
|
|
392
|
+
assignment_pairs: $after_pairs,
|
|
393
|
+
assignment_count: $after_count
|
|
394
|
+
},
|
|
395
|
+
manual_actions: [],
|
|
396
|
+
blocking_issue: $blocking
|
|
397
|
+
}'
|