@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,298 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-create.sh - INTERNAL (invoke via detect-and-create.sh, which enforces the safe_to_write guard): render Apex tokens, execute, parse created users; passwords set via separate System.setPassword, never extracted from logs. Contract + security: SKILL.md + references/apex-template-notes.md.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 4 ]; then
|
|
7
|
+
echo '{"error":"Usage: bash run-create.sh <org-alias> <count> <profile-id> <org-suffix>"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
COUNT="$2"
|
|
13
|
+
PROFILE_ID="$3"
|
|
14
|
+
SUFFIX="$4"
|
|
15
|
+
|
|
16
|
+
# Validate every arg BEFORE it is sed-substituted into anonymous Apex. These tokens are interpolated
|
|
17
|
+
# into executable Apex, so anything outside a strict grammar (Apex/sed metacharacters, code) is
|
|
18
|
+
# rejected up front rather than executed.
|
|
19
|
+
if ! [[ "$COUNT" =~ ^[1-9][0-9]{0,2}$ ]]; then
|
|
20
|
+
echo "{\"error\":\"Invalid count '$COUNT' (expected a positive integer 1-999).\"}" >&2; exit 1
|
|
21
|
+
fi
|
|
22
|
+
if ! [[ "$PROFILE_ID" =~ ^[A-Za-z0-9]{15}([A-Za-z0-9]{3})?$ ]]; then
|
|
23
|
+
echo "{\"error\":\"Invalid profile-id '$PROFILE_ID' (expected a 15- or 18-char Salesforce Id).\"}" >&2; exit 1
|
|
24
|
+
fi
|
|
25
|
+
if ! [[ "$SUFFIX" =~ ^[A-Za-z0-9]{1,18}$ ]]; then
|
|
26
|
+
echo "{\"error\":\"Invalid org-suffix '$SUFFIX' (expected 1-18 alphanumeric chars).\"}" >&2; exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
30
|
+
TEMPLATE="$SCRIPT_DIR/../assets/create-users.apex.template"
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$TEMPLATE" ]; then
|
|
33
|
+
echo "{\"error\":\"Apex template not found: $TEMPLATE\"}" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
WORK="$(mktemp -d)"
|
|
38
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
39
|
+
RESOLVED="$WORK/create-users.resolved.apex"
|
|
40
|
+
|
|
41
|
+
sed -e "s|__COUNT__|${COUNT}|g" \
|
|
42
|
+
-e "s|__PROFILE_ID__|${PROFILE_ID}|g" \
|
|
43
|
+
-e "s|__SUFFIX__|${SUFFIX}|g" \
|
|
44
|
+
"$TEMPLATE" > "$RESOLVED"
|
|
45
|
+
|
|
46
|
+
# Execute Anonymous Apex. sf apex run --json shapes differ for compile-fail / runtime-fail /
|
|
47
|
+
# success / CLI-fail; each is handled below.
|
|
48
|
+
APEX_JSON=$(sf apex run --target-org "$ORG" --file "$RESOLVED" --json 2>/dev/null || true)
|
|
49
|
+
|
|
50
|
+
# Strip C0 control chars: sf embeds an unescaped debug log in .result.logs, which jq>=1.7 rejects.
|
|
51
|
+
# This merges the log to one line; the grep below is anchored to tolerate that.
|
|
52
|
+
APEX_JSON=$(printf '%s' "$APEX_JSON" | tr -d '\000-\037')
|
|
53
|
+
|
|
54
|
+
CLI_STATUS=$(echo "$APEX_JSON" | jq -r '.status // 0' 2>/dev/null || echo "0")
|
|
55
|
+
COMPILE_SUCCESS=$(echo "$APEX_JSON" | jq -r '.result.compiled // false' 2>/dev/null || echo "false")
|
|
56
|
+
EXEC_SUCCESS=$(echo "$APEX_JSON" | jq -r '.result.success // false' 2>/dev/null || echo "false")
|
|
57
|
+
|
|
58
|
+
translate_error() {
|
|
59
|
+
local raw="$1"
|
|
60
|
+
case "$raw" in
|
|
61
|
+
*LICENSE_LIMIT_EXCEEDED*)
|
|
62
|
+
echo "Org has reached its user license limit. Free up a license slot (deactivate unused users in Setup > Users) or request a larger CDO from OrgFarm."
|
|
63
|
+
;;
|
|
64
|
+
*DUPLICATE_USERNAME*)
|
|
65
|
+
echo "Username collision with another Salesforce org (globally unique constraint). Rare - retry the skill; if it persists, another agent may be concurrently running against the same suffix."
|
|
66
|
+
;;
|
|
67
|
+
*INVALID_EMAIL_ADDRESS*)
|
|
68
|
+
echo "Salesforce rejected the @example.com email. This should never happen - likely a bug in the skill's Apex template. Escalate."
|
|
69
|
+
;;
|
|
70
|
+
*INSUFFICIENT_ACCESS*|*NoAccessException*)
|
|
71
|
+
echo "Executing user lacks required permission. Ensure the sf-CLI-authenticated user has System Administrator profile or equivalent."
|
|
72
|
+
;;
|
|
73
|
+
*INVALID_LOGIN*|*INVALID_SESSION_ID*)
|
|
74
|
+
echo "Salesforce session expired. Re-authenticate: sf org login web -a $ORG -r <my-domain-url>"
|
|
75
|
+
;;
|
|
76
|
+
*)
|
|
77
|
+
echo "$raw"
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if [ "$CLI_STATUS" != "0" ]; then
|
|
83
|
+
CLI_ERR=$(echo "$APEX_JSON" | jq -r '.message // "Unknown CLI error"' 2>/dev/null)
|
|
84
|
+
FRIENDLY=$(translate_error "$CLI_ERR")
|
|
85
|
+
echo "{\"error\":\"$FRIENDLY\",\"raw\":$(echo "$CLI_ERR" | jq -R .)}" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ "$COMPILE_SUCCESS" != "true" ]; then
|
|
90
|
+
COMPILE_ERR=$(echo "$APEX_JSON" | jq -r '.result.compileProblem // "Unknown compile error"' 2>/dev/null)
|
|
91
|
+
COMPILE_LINE=$(echo "$APEX_JSON" | jq -r '.result.line // "?"' 2>/dev/null)
|
|
92
|
+
echo "{\"error\":\"Apex compile failed at line $COMPILE_LINE: $COMPILE_ERR\"}" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [ "$EXEC_SUCCESS" != "true" ]; then
|
|
97
|
+
EXEC_ERR=$(echo "$APEX_JSON" | jq -r '.result.exceptionMessage // "Unknown execution error"' 2>/dev/null)
|
|
98
|
+
if [ -z "$EXEC_ERR" ] || [ "$EXEC_ERR" = "Unknown execution error" ] || [ "$EXEC_ERR" = "null" ]; then
|
|
99
|
+
EXEC_ERR=$(echo "$APEX_JSON" | jq -r '.message // "Unknown execution error"' 2>/dev/null)
|
|
100
|
+
fi
|
|
101
|
+
FRIENDLY=$(translate_error "$EXEC_ERR")
|
|
102
|
+
echo "{\"error\":\"$FRIENDLY\",\"raw\":$(echo "$EXEC_ERR" | jq -R .)}" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Parse AGENT_USER_CREATED|<userId>|<username>|<email> from the log; decode the HTML-encoded pipe
|
|
107
|
+
# (|) that sf emits, else the grep misses every user.
|
|
108
|
+
LOG=$(echo "$APEX_JSON" | jq -r '.result.logs // ""' 2>/dev/null | sed 's/|/|/g')
|
|
109
|
+
|
|
110
|
+
# Anchor on a real 005 Id + @example.com domain so the match ignores the Apex source echoed in the merged log.
|
|
111
|
+
CREATED_RAW=$(echo "$LOG" | grep -oE 'AGENT_USER_CREATED\|005[a-zA-Z0-9]{15}\|[^|]+@example\.com\|[^|]+@example\.com' || true)
|
|
112
|
+
|
|
113
|
+
if [ -z "$CREATED_RAW" ]; then
|
|
114
|
+
CREATED_USERS_JSON='[]'
|
|
115
|
+
else
|
|
116
|
+
CREATED_USERS_JSON=$(echo "$CREATED_RAW" \
|
|
117
|
+
| awk -F'|' '
|
|
118
|
+
BEGIN { print "[" }
|
|
119
|
+
NR > 1 { printf "," }
|
|
120
|
+
{
|
|
121
|
+
printf "{\"id\":\"%s\",\"username\":\"%s\",\"email\":\"%s\"}", $2, $3, $4
|
|
122
|
+
}
|
|
123
|
+
END { print "]" }
|
|
124
|
+
')
|
|
125
|
+
if ! echo "$CREATED_USERS_JSON" | jq -c '.' >/dev/null 2>&1; then
|
|
126
|
+
echo "{\"error\":\"Internal parse error: AGENT_USER_CREATED lines produced invalid JSON.\",\"raw_lines\":$(echo "$CREATED_RAW" | jq -Rs .)}" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
CREATED_USERS_JSON=$(echo "$CREATED_USERS_JSON" | jq -c '.')
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Set each User's password via System.setPassword (the only working path for Apex-inserted users).
|
|
133
|
+
# Fail-closed on the TraceFlag gate below; on failure the User stays active + reset_required.
|
|
134
|
+
# Full security rationale: references/apex-template-notes.md.
|
|
135
|
+
ENRICHED_USERS='[]'
|
|
136
|
+
NEEDS_RESET='[]'
|
|
137
|
+
ROLLED_BACK='[]'
|
|
138
|
+
|
|
139
|
+
# Generated passwords are written to a restricted (0600) file and NEVER returned in stdout, so
|
|
140
|
+
# plaintext cannot enter an AI/tool transcript when this leaf is invoked directly. The coordinator
|
|
141
|
+
# passes OMNI_CREDENTIALS_FILE/OMNI_CREDENTIALS_KEY to escrow into its own file; standalone runs
|
|
142
|
+
# default to ./CREDENTIALS.json. File format: { "<key>": [ {username, password}, ... ] }.
|
|
143
|
+
# Secrets never touch argv/env (both visible via process inspection): each {username,password} pair is
|
|
144
|
+
# appended as one JSON line to this 0600 temp file (inside $WORK, mode 0700) and fed to jq only via
|
|
145
|
+
# stdin / --slurpfile. $WORK is removed by the EXIT trap, so the plaintext file never persists.
|
|
146
|
+
CRED_PAIRS_FILE="$WORK/cred-pairs.jsonl"
|
|
147
|
+
: > "$CRED_PAIRS_FILE"; chmod 600 "$CRED_PAIRS_FILE" 2>/dev/null || true
|
|
148
|
+
CRED_COUNT=0
|
|
149
|
+
CRED_FILE="${OMNI_CREDENTIALS_FILE:-$PWD/CREDENTIALS.json}"
|
|
150
|
+
CRED_KEY="${OMNI_CREDENTIALS_KEY:-service-omni-agent-users-create}"
|
|
151
|
+
CRED_WRITTEN="false"
|
|
152
|
+
write_credentials() {
|
|
153
|
+
# $1 = path to a JSONL file of {username,password} objects. Secret-bearing pairs are read via
|
|
154
|
+
# --slurpfile (a file, never argv); the existing credentials file is piped in via stdin.
|
|
155
|
+
local pairs_file="$1" existing='{}' tmp
|
|
156
|
+
[ -f "$CRED_FILE" ] && existing=$(cat "$CRED_FILE" 2>/dev/null || echo '{}')
|
|
157
|
+
tmp=$(mktemp) || return 1
|
|
158
|
+
if ! printf '%s' "$existing" | jq --arg k "$CRED_KEY" --slurpfile c "$pairs_file" '. + {($k): $c}' > "$tmp" 2>/dev/null || [ ! -s "$tmp" ]; then
|
|
159
|
+
rm -f "$tmp"; return 1
|
|
160
|
+
fi
|
|
161
|
+
if ! mv "$tmp" "$CRED_FILE" 2>/dev/null; then rm -f "$tmp"; return 1; fi
|
|
162
|
+
chmod 600 "$CRED_FILE" 2>/dev/null || true
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
set_password_via_apex() {
|
|
167
|
+
# $1=user_id $2=plaintext_password. Echoes "ok" on success, else the exception text.
|
|
168
|
+
local uid="$1" pw="$2" apex_file res
|
|
169
|
+
apex_file="$WORK/setpw.apex"
|
|
170
|
+
# uid (18-char Id) and pw ([A-Za-z0-9]) can't break out of the Apex string literal.
|
|
171
|
+
printf "System.setPassword('%s', '%s');\n" "$uid" "$pw" > "$apex_file"
|
|
172
|
+
res=$(sf apex run --target-org "$ORG" --file "$apex_file" --json 2>/dev/null | tr -d '\000-\037' || true)
|
|
173
|
+
rm -f "$apex_file"
|
|
174
|
+
if [ "$(echo "$res" | jq -r '.result.success // false' 2>/dev/null)" = "true" ]; then
|
|
175
|
+
echo "ok"
|
|
176
|
+
else
|
|
177
|
+
echo "$res" | jq -r '.result.exceptionMessage // .message // "setPassword failed"' 2>/dev/null
|
|
178
|
+
fi
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# TraceFlag gate (fail-closed): prove no active TraceFlag exists for the running user BEFORE any
|
|
182
|
+
# System.setPassword, else a password literal could persist to a queryable ApexLog. Cannot prove safe
|
|
183
|
+
# => no password set, users left active + reset_required. See references/apex-template-notes.md.
|
|
184
|
+
TRACEFLAG_SAFE=false
|
|
185
|
+
TRACEFLAG_REASON="TraceFlag state could not be determined"
|
|
186
|
+
ME_UNAME=$(sf org display --target-org "$ORG" --json 2>/dev/null | jq -r '.result.username // empty' 2>/dev/null || true)
|
|
187
|
+
if [ -z "${ME_UNAME:-}" ]; then
|
|
188
|
+
TRACEFLAG_REASON="could not resolve the running user (sf org display failed)"
|
|
189
|
+
else
|
|
190
|
+
ME_UID=$(sf data query --target-org "$ORG" \
|
|
191
|
+
--query "SELECT Id FROM User WHERE Username='$ME_UNAME' LIMIT 1" \
|
|
192
|
+
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || true)
|
|
193
|
+
if [ -z "${ME_UID:-}" ]; then
|
|
194
|
+
TRACEFLAG_REASON="could not resolve the running user's Id"
|
|
195
|
+
else
|
|
196
|
+
NOW_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
197
|
+
if TF_JSON=$(sf data query --target-org "$ORG" --use-tooling-api \
|
|
198
|
+
--query "SELECT Id FROM TraceFlag WHERE TracedEntityId='$ME_UID' AND ExpirationDate > $NOW_UTC" \
|
|
199
|
+
--json 2>/dev/null) && [ -n "$TF_JSON" ]; then
|
|
200
|
+
TF_STATUS=$(echo "$TF_JSON" | jq -r '.status // 1' 2>/dev/null || echo 1)
|
|
201
|
+
if [ "$TF_STATUS" = "0" ]; then
|
|
202
|
+
TF_COUNT=$(echo "$TF_JSON" | jq -r 'if (.result.totalSize != null) then .result.totalSize elif (.result.records != null) then (.result.records | length) else "unknown" end' 2>/dev/null || echo "unknown")
|
|
203
|
+
if [ "$TF_COUNT" = "0" ]; then
|
|
204
|
+
TRACEFLAG_SAFE=true
|
|
205
|
+
TRACEFLAG_REASON=""
|
|
206
|
+
elif [ "$TF_COUNT" = "unknown" ]; then
|
|
207
|
+
TRACEFLAG_REASON="the TraceFlag query returned an unparseable result"
|
|
208
|
+
else
|
|
209
|
+
TRACEFLAG_REASON="an active Apex debug TraceFlag exists for the running user ($TF_COUNT found)"
|
|
210
|
+
fi
|
|
211
|
+
else
|
|
212
|
+
TRACEFLAG_REASON="the TraceFlag Tooling API query returned an error status"
|
|
213
|
+
fi
|
|
214
|
+
else
|
|
215
|
+
TRACEFLAG_REASON="the TraceFlag Tooling API query failed"
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
if [ "$(echo "$CREATED_USERS_JSON" | jq -r 'length')" -gt 0 ]; then
|
|
221
|
+
while IFS= read -r row; do
|
|
222
|
+
USER_ID=$(echo "$row" | jq -r '.id')
|
|
223
|
+
UNAME=$(echo "$row" | jq -r '.username')
|
|
224
|
+
EMAIL=$(echo "$row" | jq -r '.email')
|
|
225
|
+
|
|
226
|
+
if [ "$TRACEFLAG_SAFE" != "true" ]; then
|
|
227
|
+
# Fail closed: skip setPassword, keep the User active + flag for manual reset.
|
|
228
|
+
NEEDS_RESET=$(echo "$NEEDS_RESET" | jq -c --arg u "$UNAME" '. + [$u]')
|
|
229
|
+
ENRICHED_USERS=$(echo "$ENRICHED_USERS" | jq -c \
|
|
230
|
+
--arg id "$USER_ID" --arg u "$UNAME" --arg e "$EMAIL" \
|
|
231
|
+
'. + [{id: $id, username: $u, email: $e, password: null, password_status: "reset_required", password_error: "skipped: TraceFlag safety could not be proven (fail-closed)"}]')
|
|
232
|
+
continue
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# Strong [A-Za-z0-9] password + fixed mixed-class suffix (Salesforce complexity). The || true
|
|
236
|
+
# swallows tr's SIGPIPE (head closes the pipe) so pipefail+set -e don't abort mid-insert.
|
|
237
|
+
RAND="$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 14 || true)"
|
|
238
|
+
PW="${RAND}aB3"
|
|
239
|
+
SETPW_RESULT=$(set_password_via_apex "$USER_ID" "$PW")
|
|
240
|
+
|
|
241
|
+
if [ "$SETPW_RESULT" = "ok" ]; then
|
|
242
|
+
# Feed the SECRET via stdin (never argv/env); username is not secret so --arg is fine.
|
|
243
|
+
printf '%s' "$PW" | jq -Rsc --arg u "$UNAME" '{username: $u, password: .}' >> "$CRED_PAIRS_FILE"
|
|
244
|
+
CRED_COUNT=$((CRED_COUNT + 1))
|
|
245
|
+
ENRICHED_USERS=$(echo "$ENRICHED_USERS" | jq -c \
|
|
246
|
+
--arg id "$USER_ID" --arg u "$UNAME" --arg e "$EMAIL" \
|
|
247
|
+
'. + [{id: $id, username: $u, email: $e, password: null, password_status: "generated"}]')
|
|
248
|
+
else
|
|
249
|
+
# Non-destructive: keep the active User, flag it for a manual password reset.
|
|
250
|
+
NEEDS_RESET=$(echo "$NEEDS_RESET" | jq -c --arg u "$UNAME" '. + [$u]')
|
|
251
|
+
ENRICHED_USERS=$(echo "$ENRICHED_USERS" | jq -c \
|
|
252
|
+
--arg id "$USER_ID" --arg u "$UNAME" --arg e "$EMAIL" --arg r "$SETPW_RESULT" \
|
|
253
|
+
'. + [{id: $id, username: $u, email: $e, password: null, password_status: "reset_required", password_error: $r}]')
|
|
254
|
+
fi
|
|
255
|
+
done < <(echo "$CREATED_USERS_JSON" | jq -c '.[]')
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Escrow generated passwords to the restricted file; never emit plaintext in stdout. Fail-closed:
|
|
259
|
+
# if escrow fails, the passwords were set on-org but cannot be handed back, so flag those users for a
|
|
260
|
+
# manual reset rather than leaking the plaintext.
|
|
261
|
+
CRED_WARNING=""
|
|
262
|
+
if [ "$CRED_COUNT" -gt 0 ]; then
|
|
263
|
+
if write_credentials "$CRED_PAIRS_FILE"; then
|
|
264
|
+
CRED_WRITTEN="true"
|
|
265
|
+
else
|
|
266
|
+
CRED_WARNING="Passwords were set on the created users but could NOT be escrowed to the credentials file ($CRED_FILE); plaintext was deliberately NOT emitted to stdout. Reset these users' passwords in Setup > Users."
|
|
267
|
+
# Escrow failed → the plaintext is unrecoverable, so these users genuinely need a manual reset:
|
|
268
|
+
# add them to users_needing_password_reset (not just the per-user status) before flipping status.
|
|
269
|
+
ESCROW_FAILED_UNAMES=$(echo "$ENRICHED_USERS" | jq -c '[ .[] | select(.password_status=="generated") | .username ]')
|
|
270
|
+
NEEDS_RESET=$(jq -c -n --argjson a "$NEEDS_RESET" --argjson b "$ESCROW_FAILED_UNAMES" '$a + $b | unique')
|
|
271
|
+
ENRICHED_USERS=$(echo "$ENRICHED_USERS" | jq -c '[ .[] | if .password_status=="generated" then (.password_status="reset_required" | .password_error="escrow_failed") else . end ]')
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# Surface why password generation was skipped when the TraceFlag gate failed.
|
|
276
|
+
SECURITY_WARNING=""
|
|
277
|
+
if [ "$TRACEFLAG_SAFE" != "true" ]; then
|
|
278
|
+
SECURITY_WARNING="Password generation was SKIPPED (fail-closed) because ${TRACEFLAG_REASON}. No System.setPassword was executed, so no plaintext could enter an ApexLog. The created users are ACTIVE and flagged password_status:\"reset_required\" - set their passwords in Setup > Users after confirming no debug TraceFlag is active, or disable the TraceFlag and re-run."
|
|
279
|
+
fi
|
|
280
|
+
[ -n "$CRED_WARNING" ] && SECURITY_WARNING="${SECURITY_WARNING:+$SECURITY_WARNING }$CRED_WARNING"
|
|
281
|
+
|
|
282
|
+
jq -n \
|
|
283
|
+
--argjson created "$ENRICHED_USERS" \
|
|
284
|
+
--argjson needs_reset "$NEEDS_RESET" \
|
|
285
|
+
--argjson rolled_back "$ROLLED_BACK" \
|
|
286
|
+
--arg security_warning "$SECURITY_WARNING" \
|
|
287
|
+
--arg cred_written "$CRED_WRITTEN" \
|
|
288
|
+
--arg cred_file "$CRED_FILE" \
|
|
289
|
+
'{
|
|
290
|
+
created_users: $created,
|
|
291
|
+
created_count: ($created | length),
|
|
292
|
+
users_needing_password_reset: $needs_reset,
|
|
293
|
+
rolled_back_users: $rolled_back,
|
|
294
|
+
credentials_written: ($cred_written == "true"),
|
|
295
|
+
credentials_file: (if $cred_written == "true" then $cred_file else null end),
|
|
296
|
+
security_warning: (if $security_warning == "" then null else $security_warning end),
|
|
297
|
+
security_note: "Passwords are set via anonymous Apex System.setPassword. The plaintext appears in the inline executeAnonymous debug log and, ONLY when a debug-log TraceFlag is active for the running user, in a queryable ApexLog row. To avoid ever persisting plaintext to a log, this script FAILS CLOSED BEFORE the first System.setPassword: it proves via a SOQL-filtered Tooling API query (ExpirationDate > now) that no active TraceFlag exists for the running user. If safety cannot be positively proven - the running user cannot be resolved, the Tooling API query fails or is unparseable, or any active TraceFlag exists - NO password is generated at all; the created users are left ACTIVE and flagged password_status:\"reset_required\", and security_warning explains why. Generated passwords are NEVER returned in this output; they are written to a restricted 0600 credentials_file (default ./CREDENTIALS.json, or OMNI_CREDENTIALS_FILE) so plaintext cannot enter a tool/AI transcript. Read the file once, distribute securely, then delete it. Set passwords for reset_required users in Setup > Users after ensuring no debug TraceFlag is active (or disable the TraceFlag and re-run)."
|
|
298
|
+
}'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Org-free security tests proving the TraceFlag fail-closed contract for both run-create.sh scripts: setPassword runs only when no active TraceFlag exists (else users stay reset_required; a failing TraceFlag query also fails closed), proven via a fake-sf marker file."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
SKILLS_ROOT = Path(__file__).resolve().parents[3] # the skills/ directory
|
|
12
|
+
AGENT_SCRIPT = SKILLS_ROOT / "service-omni-agent-users-create/scripts/run-create.sh"
|
|
13
|
+
SUPERVISOR_SCRIPT = SKILLS_ROOT / "service-omni-supervisor-users-create/scripts/run-create.sh"
|
|
14
|
+
|
|
15
|
+
FAKE_SF = r"""#!/usr/bin/env bash
|
|
16
|
+
# Minimal fake `sf` for TraceFlag fail-closed tests. Differentiates the create-users Apex from the
|
|
17
|
+
# System.setPassword Apex by reading the --file contents, and records setPassword submissions.
|
|
18
|
+
cmd="$1"; sub="$2"; shift 2 || true
|
|
19
|
+
all="$*"
|
|
20
|
+
file=""
|
|
21
|
+
prev=""
|
|
22
|
+
for a in "$@"; do
|
|
23
|
+
if [ "$prev" = "--file" ]; then file="$a"; fi
|
|
24
|
+
prev="$a"
|
|
25
|
+
done
|
|
26
|
+
case "$cmd $sub" in
|
|
27
|
+
"apex run")
|
|
28
|
+
# The setPassword Apex is EXACTLY `System.setPassword('id','pw');` on its first line; the
|
|
29
|
+
# create-users template may merely mention setPassword in a comment, so match the first line only.
|
|
30
|
+
if [ -n "$file" ] && head -1 "$file" 2>/dev/null | grep -q '^System.setPassword'; then
|
|
31
|
+
: > "$FAKE_SETPW_MARKER"
|
|
32
|
+
echo '{"status":0,"result":{"compiled":true,"success":true,"logs":""}}'
|
|
33
|
+
else
|
|
34
|
+
echo '{"status":0,"result":{"compiled":true,"success":true,"logs":"AGENT_USER_CREATED|005000000000001AAA|agent1.sfx@example.com|agent1.sfx@example.com SUPERVISOR_CREATED|005000000000002AAA|supervisor1.sfx@example.com|supervisor1.sfx@example.com"}}'
|
|
35
|
+
fi
|
|
36
|
+
;;
|
|
37
|
+
"org display")
|
|
38
|
+
echo '{"status":0,"result":{"username":"admin@example.com"}}'
|
|
39
|
+
;;
|
|
40
|
+
"data query")
|
|
41
|
+
if printf '%s' "$all" | grep -q "TraceFlag"; then
|
|
42
|
+
case "${FAKE_TF:-none}" in
|
|
43
|
+
active) echo '{"status":0,"result":{"totalSize":1,"records":[{"Id":"7tf000000000001AAA","ExpirationDate":"2999-01-01T12:00:00.000+0000"}]}}' ;;
|
|
44
|
+
fail) exit 1 ;;
|
|
45
|
+
*) echo '{"status":0,"result":{"totalSize":0,"records":[]}}' ;;
|
|
46
|
+
esac
|
|
47
|
+
else
|
|
48
|
+
echo '{"status":0,"result":{"records":[{"Id":"005000000000009AAA"}]}}'
|
|
49
|
+
fi
|
|
50
|
+
;;
|
|
51
|
+
*)
|
|
52
|
+
echo '{"status":0,"result":{}}'
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run(script: Path, tf_mode: str):
|
|
59
|
+
"""Run a user-create script with the fake sf; return (rc, parsed_json, setpw_called)."""
|
|
60
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
61
|
+
bindir = Path(tmp) / "bin"
|
|
62
|
+
bindir.mkdir()
|
|
63
|
+
(bindir / "sf").write_text(FAKE_SF)
|
|
64
|
+
(bindir / "sf").chmod(0o755)
|
|
65
|
+
marker = Path(tmp) / "setpw_called"
|
|
66
|
+
|
|
67
|
+
cred_file = Path(tmp) / "CREDENTIALS.json"
|
|
68
|
+
env = dict(os.environ)
|
|
69
|
+
env["PATH"] = f"{bindir}:{env['PATH']}"
|
|
70
|
+
env["FAKE_TF"] = tf_mode
|
|
71
|
+
env["FAKE_SETPW_MARKER"] = str(marker)
|
|
72
|
+
# Isolate the credentials escrow file inside the temp dir so a run never writes to the repo.
|
|
73
|
+
env["OMNI_CREDENTIALS_FILE"] = str(cred_file)
|
|
74
|
+
|
|
75
|
+
proc = subprocess.run(
|
|
76
|
+
["bash", str(script), "myorg", "1", "00e000000000001AAA", "sfx"],
|
|
77
|
+
capture_output=True, text=True, env=env, cwd=tmp,
|
|
78
|
+
)
|
|
79
|
+
out = None
|
|
80
|
+
try:
|
|
81
|
+
out = json.loads(proc.stdout)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
pass
|
|
84
|
+
cred_text = cred_file.read_text() if cred_file.exists() else ""
|
|
85
|
+
return proc.returncode, out, marker.exists(), proc.stdout + proc.stderr, cred_text
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TraceFlagFailClosedTests(unittest.TestCase):
|
|
89
|
+
SCRIPTS = (("agent", AGENT_SCRIPT), ("supervisor", SUPERVISOR_SCRIPT))
|
|
90
|
+
|
|
91
|
+
def test_no_traceflag_sets_password(self):
|
|
92
|
+
for label, script in self.SCRIPTS:
|
|
93
|
+
with self.subTest(script=label):
|
|
94
|
+
rc, out, setpw_called, raw, cred_text = _run(script, "none")
|
|
95
|
+
self.assertEqual(rc, 0, raw)
|
|
96
|
+
self.assertIsNotNone(out, raw)
|
|
97
|
+
self.assertTrue(setpw_called, "System.setPassword must run when no TraceFlag is active")
|
|
98
|
+
self.assertIsNone(out.get("security_warning"), raw)
|
|
99
|
+
self.assertEqual(out["created_users"][0]["password_status"], "generated", raw)
|
|
100
|
+
# P0: plaintext must NEVER be in stdout - it is escrowed to the 0600 credentials file.
|
|
101
|
+
self.assertIsNone(out["created_users"][0]["password"], raw)
|
|
102
|
+
self.assertTrue(out.get("credentials_written"), raw)
|
|
103
|
+
self.assertTrue(str(out.get("credentials_file", "")).endswith("CREDENTIALS.json"), raw)
|
|
104
|
+
pw = json.loads(cred_text)
|
|
105
|
+
pairs = next(iter(pw.values()))
|
|
106
|
+
self.assertTrue(pairs and pairs[0].get("password"), "escrow file must hold the plaintext")
|
|
107
|
+
self.assertNotIn(pairs[0]["password"], raw, "generated password leaked into stdout/stderr")
|
|
108
|
+
|
|
109
|
+
def test_active_traceflag_never_calls_setpassword(self):
|
|
110
|
+
for label, script in self.SCRIPTS:
|
|
111
|
+
with self.subTest(script=label):
|
|
112
|
+
rc, out, setpw_called, raw, cred_text = _run(script, "active")
|
|
113
|
+
self.assertEqual(rc, 0, raw)
|
|
114
|
+
self.assertIsNotNone(out, raw)
|
|
115
|
+
self.assertFalse(setpw_called,
|
|
116
|
+
"PROOF FAILED: System.setPassword was submitted while a TraceFlag was active")
|
|
117
|
+
self.assertIsNotNone(out.get("security_warning"), raw)
|
|
118
|
+
self.assertEqual(out["created_users"][0]["password_status"], "reset_required", raw)
|
|
119
|
+
self.assertIsNone(out["created_users"][0]["password"], raw)
|
|
120
|
+
|
|
121
|
+
def test_traceflag_query_failure_fails_closed(self):
|
|
122
|
+
for label, script in self.SCRIPTS:
|
|
123
|
+
with self.subTest(script=label):
|
|
124
|
+
rc, out, setpw_called, raw, cred_text = _run(script, "fail")
|
|
125
|
+
self.assertEqual(rc, 0, raw)
|
|
126
|
+
self.assertIsNotNone(out, raw)
|
|
127
|
+
self.assertFalse(setpw_called,
|
|
128
|
+
"A failed TraceFlag query must fail closed (no setPassword)")
|
|
129
|
+
self.assertIsNotNone(out.get("security_warning"), raw)
|
|
130
|
+
self.assertEqual(out["created_users"][0]["password_status"], "reset_required", raw)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SourceContractTests(unittest.TestCase):
|
|
134
|
+
"""Guard the exact fail-closed shape so a refactor cannot silently regress it."""
|
|
135
|
+
|
|
136
|
+
def test_setpassword_is_gated_on_traceflag_safe(self):
|
|
137
|
+
for script in (AGENT_SCRIPT, SUPERVISOR_SCRIPT):
|
|
138
|
+
src = script.read_text()
|
|
139
|
+
self.assertIn('if [ "$TRACEFLAG_SAFE" != "true" ]; then', src, str(script))
|
|
140
|
+
# The active-TraceFlag path must `continue` before reaching set_password_via_apex.
|
|
141
|
+
gate = src.index('if [ "$TRACEFLAG_SAFE" != "true" ]; then')
|
|
142
|
+
setpw = src.index("SETPW_RESULT=$(set_password_via_apex")
|
|
143
|
+
self.assertLess(gate, setpw, f"{script}: TRACEFLAG_SAFE gate must precede setPassword")
|
|
144
|
+
|
|
145
|
+
def test_traceflag_query_uses_soql_expiration_filter(self):
|
|
146
|
+
for script in (AGENT_SCRIPT, SUPERVISOR_SCRIPT):
|
|
147
|
+
src = script.read_text()
|
|
148
|
+
self.assertIn("ExpirationDate > $NOW_UTC", src, str(script))
|
|
149
|
+
self.assertIn("TRACEFLAG_SAFE=false", src, str(script))
|
|
150
|
+
|
|
151
|
+
def test_password_never_passed_via_jq_argv(self):
|
|
152
|
+
# The generated password must not reach jq (or any command) through argv/env, which are visible
|
|
153
|
+
# via process inspection. It is fed to jq over stdin and escrowed via --slurpfile (a file).
|
|
154
|
+
for script in (AGENT_SCRIPT, SUPERVISOR_SCRIPT):
|
|
155
|
+
src = script.read_text()
|
|
156
|
+
self.assertNotIn('--arg p "$PW"', src, f"{script}: password must not be a jq --arg")
|
|
157
|
+
self.assertNotIn('--argjson c "$pairs"', src, f"{script}: pairs (with secrets) must not be jq argv")
|
|
158
|
+
self.assertIn('printf \'%s\' "$PW" | jq -Rsc', src, f"{script}: password must be fed via stdin")
|
|
159
|
+
self.assertIn('--slurpfile c "$pairs_file"', src, f"{script}: escrow must read pairs from a file")
|
|
160
|
+
|
|
161
|
+
def test_escrow_failure_marks_users_needing_reset(self):
|
|
162
|
+
# When escrow fails the plaintext is unrecoverable, so the affected users must be listed in
|
|
163
|
+
# users_needing_password_reset (not only flipped in per-user status).
|
|
164
|
+
for script in (AGENT_SCRIPT, SUPERVISOR_SCRIPT):
|
|
165
|
+
src = script.read_text()
|
|
166
|
+
self.assertIn("ESCROW_FAILED_UNAMES=", src, str(script))
|
|
167
|
+
self.assertIn("NEEDS_RESET=$(jq -c -n --argjson a \"$NEEDS_RESET\" --argjson b \"$ESCROW_FAILED_UNAMES\"", src, str(script))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-omni-agent-work-sharing-configure
|
|
3
|
+
description: "Use to configure or verify org-wide AgentWork visibility for supervisors after explicit confirmation. Triggers: let supervisors read AgentWork, configure AgentWork sharing, verify AgentWork OWD, make AgentWork visible to supervisors, change AgentWork internal access. Do not use to filter which reps appear in Command Center; use service-omni-supervisor-config-deploy for that."
|
|
4
|
+
allowed-tools: Bash Read Write Edit Glob Grep
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1"
|
|
7
|
+
minApiVersion: "66.0"
|
|
8
|
+
relatedSkills:
|
|
9
|
+
- "service-omni-supervisor-config-deploy"
|
|
10
|
+
accessCheck:
|
|
11
|
+
- type: license
|
|
12
|
+
value: ServiceCloud
|
|
13
|
+
cliTools:
|
|
14
|
+
- tool: ["jq"]
|
|
15
|
+
semver: ">=1.6"
|
|
16
|
+
- tool: ["python3"]
|
|
17
|
+
semver: ">=3.8"
|
|
18
|
+
- tool: ["sf"]
|
|
19
|
+
semver: ">=2.139.6"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# service-omni-agent-work-sharing-configure
|
|
23
|
+
|
|
24
|
+
Detect and optionally configure the standard `AgentWork` object's internal organization-wide default. The supported headless contract is `CustomObject:AgentWork` metadata: retrieve the current metadata, preserve `externalSharingModel`, deploy internal `sharingModel=Read`, and retrieve it again for proof.
|
|
25
|
+
|
|
26
|
+
This setting grants read visibility broadly to internal users who already have object access. It is not a substitute for an `OmniSupervisorConfig`: that configuration filters which reps, queues, skills, actions, and tabs a supervisor sees, but does not grant record access.
|
|
27
|
+
|
|
28
|
+
## Inputs
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bash scripts/configure-and-report.sh <org-alias> --plan
|
|
32
|
+
bash scripts/configure-and-report.sh <org-alias> --confirm-org-wide-visibility
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `org-alias` is required and must already be authenticated with `sf`.
|
|
36
|
+
- `--plan` retrieves and reports the current and desired models without deploying.
|
|
37
|
+
- `--confirm-org-wide-visibility` is required only when a run must change `Private` to `Read`. Omitting it blocks before deployment.
|
|
38
|
+
|
|
39
|
+
## Safety contract
|
|
40
|
+
|
|
41
|
+
- A run refuses production customer orgs. Plan mode remains read-only.
|
|
42
|
+
- `externalSharingModel` is preserved exactly.
|
|
43
|
+
- `Read` and `ReadWrite` already provide internal read visibility and are reused. The skill never narrows `ReadWrite` to `Read`.
|
|
44
|
+
- The only supported mutation is `Private` to `Read`; the skill never grants internal edit access.
|
|
45
|
+
- Failed classification, retrieve, deploy, or read-back blocks. It never reports success from an inconclusive response.
|
|
46
|
+
- Use a dedicated sandbox or test org because changing an OWD can initiate sharing recalculation.
|
|
47
|
+
|
|
48
|
+
## Workflow
|
|
49
|
+
|
|
50
|
+
1. Classify the target org and enforce the production-write guard.
|
|
51
|
+
2. Retrieve `CustomObject:AgentWork` through Metadata API.
|
|
52
|
+
3. Read internal and external sharing models.
|
|
53
|
+
4. Reuse `Read` or `ReadWrite` with no deploy.
|
|
54
|
+
5. In plan mode, report the proposed `Private` to `Read` change and exit yellow.
|
|
55
|
+
6. In run mode, require explicit blast-radius confirmation.
|
|
56
|
+
7. Deploy only the standard-object metadata containing preserved external sharing and internal `Read`.
|
|
57
|
+
8. Retrieve again and require internal `Read` plus the unchanged external model.
|
|
58
|
+
|
|
59
|
+
## Output
|
|
60
|
+
|
|
61
|
+
A single JSON object containing `status`, `previous_sharing_model`, `sharing_model`, `external_sharing_model`, `changed`, `dry_run`, `deploy_id`, and `blocking_issue`.
|
|
62
|
+
|
|
63
|
+
- `configured`: a confirmed `Private` to `Read` deployment succeeded and read-back matched.
|
|
64
|
+
- `reused`: current `Read` or `ReadWrite` already provides visibility.
|
|
65
|
+
- `action_needed`: plan mode found `Private`; no write occurred.
|
|
66
|
+
- `blocked`: a safety guard or supported-API verification failed.
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
- This skill changes visibility, not supervisor scope. Use the supervisor configuration skill for filtering.
|
|
71
|
+
- It does not create sharing rules or per-record `AgentWorkShare` rows.
|
|
72
|
+
- It cannot prove a particular supervisor's complete UI access; validate the end-to-end Command Center journey with the intended supervisor user after configuration.
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
| File | When to read |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `references/api-notes.md` | Before approving the org-wide change or diagnosing a retrieve/deploy failure |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# AgentWork sharing API notes
|
|
2
|
+
|
|
3
|
+
## Supported contract
|
|
4
|
+
|
|
5
|
+
`AgentWork` is a standard object whose organization-wide defaults round-trip through the `CustomObject` Metadata API representation:
|
|
6
|
+
|
|
7
|
+
```xml
|
|
8
|
+
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
9
|
+
<externalSharingModel>Private</externalSharingModel>
|
|
10
|
+
<sharingModel>Read</sharingModel>
|
|
11
|
+
</CustomObject>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Retrieve with `CustomObject:AgentWork`. A deployment must preserve the retrieved `externalSharingModel`; only the internal `sharingModel` is changed.
|
|
15
|
+
|
|
16
|
+
## Access semantics
|
|
17
|
+
|
|
18
|
+
- `Private`: object permissions alone do not make every AgentWork record readable.
|
|
19
|
+
- `Read`: Public Read Only for internal access.
|
|
20
|
+
- `ReadWrite`: already includes read access, so this skill leaves it unchanged instead of narrowing access.
|
|
21
|
+
|
|
22
|
+
Object permissions, supervisor permissions, and supervisor configuration filters still apply. An OWD change does not grant object permission and does not select which reps appear in Command Center.
|
|
23
|
+
|
|
24
|
+
## Verification
|
|
25
|
+
|
|
26
|
+
A successful deploy response is insufficient. Retrieve `CustomObject:AgentWork` again and require:
|
|
27
|
+
|
|
28
|
+
- `sharingModel=Read` after a change.
|
|
29
|
+
- `externalSharingModel` equal to its pre-deploy value.
|
|
30
|
+
|
|
31
|
+
After metadata verification, sign in as the intended supervisor and validate that the expected work is visible and unrelated supervisor scopes remain filtered correctly.
|