@salesforce/afv-skills 1.47.0 → 1.48.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 +7 -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
package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Idempotent in-place patcher for canEscalate + outboundRoute surfaces; refuses (exit 4) if no safe insertion point. XSD order + modes: references/classifier-contracts.md.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const [mode, filePath, flowName, escalationMessageArg] = process.argv.slice(2);
|
|
7
|
+
if (!mode || !filePath) {
|
|
8
|
+
process.stderr.write('usage: node patch-escalation-surfaces.mjs <canEscalate|outboundRoute> <file> [flowApiName] [escalationMessage]\n');
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const xmlEscape = (s) => String(s)
|
|
13
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
14
|
+
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
text = readFileSync(filePath, 'utf8');
|
|
18
|
+
} catch {
|
|
19
|
+
process.stderr.write(`error: cannot read ${filePath}\n`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const done = (summary) => { process.stdout.write(JSON.stringify(summary) + '\n'); process.exit(0); };
|
|
24
|
+
const refuse = (msg) => { process.stderr.write(`refuse: ${msg}\n`); process.exit(4); };
|
|
25
|
+
|
|
26
|
+
// Return {index, indent} of the earliest top-level opening tag from `tags` found in `hay`
|
|
27
|
+
// (searched from offset `from`), along with the leading whitespace on that tag's line.
|
|
28
|
+
const firstTag = (hay, tags, from = 0) => {
|
|
29
|
+
const scope = hay.slice(from);
|
|
30
|
+
let best = null;
|
|
31
|
+
for (const t of tags) {
|
|
32
|
+
const re = new RegExp(`([ \\t]*)<${t}(?=[ >/])`, 'g');
|
|
33
|
+
for (const m of scope.matchAll(re)) {
|
|
34
|
+
const idx = from + m.index;
|
|
35
|
+
if (best === null || idx < best.index) best = { index: idx, indent: m[1] || '' };
|
|
36
|
+
break; // first match of this tag is enough
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (mode === 'canEscalate') {
|
|
43
|
+
// Idempotent: normalize an existing <canEscalate> to true.
|
|
44
|
+
if (/<canEscalate>[\s\S]*?<\/canEscalate>/.test(text)) {
|
|
45
|
+
const next = text.replace(/<canEscalate>[\s\S]*?<\/canEscalate>/, '<canEscalate>true</canEscalate>');
|
|
46
|
+
if (next !== text) writeFileSync(filePath, next);
|
|
47
|
+
done({ mode, changed: next !== text, action: 'set-existing' });
|
|
48
|
+
}
|
|
49
|
+
const rootOpen = text.match(/<GenAiPlugin\b[^>]*>/);
|
|
50
|
+
if (!rootOpen) refuse('no <GenAiPlugin> root element found');
|
|
51
|
+
let searchFrom = rootOpen.index + rootOpen[0].length;
|
|
52
|
+
// Skip PAST any <aiPluginUtterances> blocks first - they legally precede canEscalate and each
|
|
53
|
+
// one carries a nested <developerName>, which would otherwise be mistaken for the top-level anchor.
|
|
54
|
+
const utteranceCloses = [...text.matchAll(/<\/aiPluginUtterances>/g)];
|
|
55
|
+
if (utteranceCloses.length) {
|
|
56
|
+
const last = utteranceCloses[utteranceCloses.length - 1];
|
|
57
|
+
searchFrom = last.index + '</aiPluginUtterances>'.length;
|
|
58
|
+
}
|
|
59
|
+
// canEscalate must come BEFORE the first of these top-level siblings.
|
|
60
|
+
const anchor = firstTag(text, [
|
|
61
|
+
'description', 'developerName', 'genAiFunctions', 'genAiPluginInstructions',
|
|
62
|
+
'language', 'localActionLinks', 'localActions', 'localDeveloperName',
|
|
63
|
+
'masterLabel', 'pluginType', 'scope', 'source',
|
|
64
|
+
], searchFrom);
|
|
65
|
+
if (!anchor) refuse('no post-canEscalate sibling (description/developerName/masterLabel/...) found; refusing to guess placement');
|
|
66
|
+
const indent = anchor.indent || ' ';
|
|
67
|
+
const insertion = `${indent}<canEscalate>true</canEscalate>\n`;
|
|
68
|
+
const followingTag = text.slice(anchor.index).match(/<([A-Za-z]+)/)[1];
|
|
69
|
+
const next = text.slice(0, anchor.index) + insertion + text.slice(anchor.index);
|
|
70
|
+
writeFileSync(filePath, next);
|
|
71
|
+
done({ mode, changed: true, action: 'inserted-before-' + followingTag });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (mode === 'outboundRoute') {
|
|
75
|
+
if (!flowName) { process.stderr.write('error: outboundRoute requires <flowApiName>\n'); process.exit(2); }
|
|
76
|
+
const escMsg = xmlEscape(escalationMessageArg || 'Transferring you to a live support agent.');
|
|
77
|
+
const MESSAGING_SURFACES = new Set(['Messaging', 'CustomerWebClient']);
|
|
78
|
+
|
|
79
|
+
const surfaces = [...text.matchAll(/<plannerSurfaces>([\s\S]*?)<\/plannerSurfaces>/g)];
|
|
80
|
+
if (surfaces.length === 0) refuse('no <plannerSurfaces> block found; refusing to invent one');
|
|
81
|
+
const chosen = surfaces.find((m) => {
|
|
82
|
+
const t = m[1].match(/<surfaceType>\s*([^<]*?)\s*<\/surfaceType>/);
|
|
83
|
+
return t && MESSAGING_SURFACES.has(t[1].trim());
|
|
84
|
+
});
|
|
85
|
+
if (!chosen) refuse('no Messaging-class planner surface (surfaceType Messaging|CustomerWebClient) found');
|
|
86
|
+
|
|
87
|
+
const innerStart = chosen.index + '<plannerSurfaces>'.length;
|
|
88
|
+
const innerEnd = chosen.index + chosen[0].length - '</plannerSurfaces>'.length;
|
|
89
|
+
const inner = text.slice(innerStart, innerEnd);
|
|
90
|
+
|
|
91
|
+
// Idempotent: an existing outboundRouteConfigs already targeting this flow -> normalize the type only.
|
|
92
|
+
const existing = [...inner.matchAll(/<outboundRouteConfigs>([\s\S]*?)<\/outboundRouteConfigs>/g)]
|
|
93
|
+
.find((c) => {
|
|
94
|
+
const n = c[1].match(/<outboundRouteName>\s*([^<]*?)\s*<\/outboundRouteName>/);
|
|
95
|
+
return n && n[1].trim() === flowName;
|
|
96
|
+
});
|
|
97
|
+
if (existing) {
|
|
98
|
+
const block = existing[0];
|
|
99
|
+
let newBlock = block;
|
|
100
|
+
if (/<outboundRouteType>[\s\S]*?<\/outboundRouteType>/.test(block)) {
|
|
101
|
+
newBlock = block.replace(/<outboundRouteType>[\s\S]*?<\/outboundRouteType>/, '<outboundRouteType>OmniChannelFlow</outboundRouteType>');
|
|
102
|
+
} else {
|
|
103
|
+
newBlock = block.replace(/<\/outboundRouteConfigs>/, ' <outboundRouteType>OmniChannelFlow</outboundRouteType>\n </outboundRouteConfigs>');
|
|
104
|
+
}
|
|
105
|
+
if (newBlock === block) done({ mode, changed: false, action: 'already-configured' });
|
|
106
|
+
const newInner = inner.replace(block, newBlock);
|
|
107
|
+
const next = text.slice(0, innerStart) + newInner + text.slice(innerEnd);
|
|
108
|
+
writeFileSync(filePath, next);
|
|
109
|
+
done({ mode, changed: true, action: 'normalized-type' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Insert a fresh block BEFORE <surface> to honor the AiPlannerSurfaceDef XSD sequence
|
|
113
|
+
// (...outboundRouteConfigs*, surface, surfaceType). <surface> is required, so it must be present.
|
|
114
|
+
const surfaceAnchor = firstTag(inner, ['surface']);
|
|
115
|
+
if (!surfaceAnchor) refuse('Messaging planner surface has no <surface> element; refusing to guess insertion point');
|
|
116
|
+
const indent = surfaceAnchor.indent || ' ';
|
|
117
|
+
const childIndent = indent + ' ';
|
|
118
|
+
const block =
|
|
119
|
+
`${indent}<outboundRouteConfigs>\n` +
|
|
120
|
+
`${childIndent}<escalationMessage>${escMsg}</escalationMessage>\n` +
|
|
121
|
+
`${childIndent}<outboundRouteName>${xmlEscape(flowName)}</outboundRouteName>\n` +
|
|
122
|
+
`${childIndent}<outboundRouteType>OmniChannelFlow</outboundRouteType>\n` +
|
|
123
|
+
`${indent}</outboundRouteConfigs>\n`;
|
|
124
|
+
const newInner = inner.slice(0, surfaceAnchor.index) + block + inner.slice(surfaceAnchor.index);
|
|
125
|
+
const next = text.slice(0, innerStart) + newInner + text.slice(innerEnd);
|
|
126
|
+
writeFileSync(filePath, next);
|
|
127
|
+
done({ mode, changed: true, action: 'inserted-before-surface' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.stderr.write(`error: unknown mode '${mode}'\n`);
|
|
131
|
+
process.exit(2);
|
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Shared paths + fake-`sf` harness for the escalation-configure contract suite (org-free, network-free; drives the bash+Node skill via subprocess, asserting exit codes + emitted JSON)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# skills/ root is three levels up (.../<skill>/scripts/tests/_bootstrap.py).
|
|
10
|
+
SKILLS_ROOT = Path(__file__).resolve().parents[3]
|
|
11
|
+
|
|
12
|
+
SKILL_DIR = "service-agentforce-human-escalation-configure"
|
|
13
|
+
SCRIPT = f"{SKILL_DIR}/scripts/verify-and-configure.sh"
|
|
14
|
+
# The ITSM coordinator's advancement gate - exercised for the escalation verdict mapping.
|
|
15
|
+
VERDICT_SCRIPT = "service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs"
|
|
16
|
+
|
|
17
|
+
# A fake `sf` that reports a PRODUCTION customer org so the safe_to_write guard must refuse.
|
|
18
|
+
# A fake `project deploy` returns a sentinel so a guard bypass would surface loudly.
|
|
19
|
+
_FAKE_SF_PROD = r"""#!/usr/bin/env bash
|
|
20
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
21
|
+
case "$sub" in
|
|
22
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
23
|
+
"data query")
|
|
24
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
25
|
+
echo '{"status":0,"result":{"records":[{"Id":"00Dxx0000000000EAA","IsSandbox":false,"TrialExpirationDate":null,"OrganizationType":"Enterprise Edition"}]}}'
|
|
26
|
+
else
|
|
27
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
28
|
+
fi
|
|
29
|
+
exit 0;;
|
|
30
|
+
"project deploy")
|
|
31
|
+
echo '{"status":1,"result":{"success":false},"message":"FAKE_SF_DEPLOY_SHOULD_NOT_BE_REACHED"}'; exit 1;;
|
|
32
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
33
|
+
esac
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# A fake `sf` for a SANDBOX org where every deterministic surface already resolves (active agent,
|
|
37
|
+
# MessagingChannel, queue + QueueSobject + QRC, active RoutingFlow) and `project retrieve` writes
|
|
38
|
+
# canEscalate=true / an outboundRouteName so the doc-driven surfaces verify true. The FAKE_* env vars
|
|
39
|
+
# below toggle each surface off to exercise the block/create paths; a GroupMember POST flips membership
|
|
40
|
+
# via a marker file (stateful convergence); canonical QRC id 0RC000000000001AAA makes create+adopt converge.
|
|
41
|
+
_FAKE_SF_SANDBOX = r"""#!/usr/bin/env bash
|
|
42
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
43
|
+
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
AGENT_ACTIVE="${FAKE_AGENT_ACTIVE:-1}"
|
|
45
|
+
MC_PRESENT="${FAKE_MC_PRESENT:-1}"
|
|
46
|
+
QS_PRESENT="${FAKE_QS_PRESENT:-1}"
|
|
47
|
+
QRC_PRESENT="${FAKE_QRC_PRESENT:-1}"
|
|
48
|
+
QUEUE_PRESENT="${FAKE_QUEUE_PRESENT:-1}"
|
|
49
|
+
QUEUE_BOUND="${FAKE_QUEUE_BOUND:-1}"
|
|
50
|
+
QUEUE_MEMBERS="${FAKE_QUEUE_MEMBERS:-1}"
|
|
51
|
+
ROUTE_TYPE="${FAKE_ROUTE_TYPE:-OmniChannelFlow}"
|
|
52
|
+
ROUTE_SURFACE="${FAKE_ROUTE_SURFACE:-Messaging}"
|
|
53
|
+
# FAKE_GENAI_MD=0 → org exposes no GenAiPlugin/GenAiPlannerBundle metadata types (the escalation
|
|
54
|
+
# preflight must then BLOCK before any write). Default 1 → the authoring metadata is exposed.
|
|
55
|
+
GENAI_MD="${FAKE_GENAI_MD:-1}"
|
|
56
|
+
# FAKE_MODEL pins the authoring model the org advertises: classic (GenAiPlugin+GenAiPlannerBundle),
|
|
57
|
+
# nga (AiAuthoringBundle, no GenAiPlannerBundle), or none. Empty => derive from GENAI_MD (1=classic,0=none).
|
|
58
|
+
MODEL="${FAKE_MODEL:-}"
|
|
59
|
+
if [ -z "$MODEL" ]; then [ "$GENAI_MD" = "1" ] && MODEL="classic" || MODEL="none"; fi
|
|
60
|
+
TARGET_MODEL="${FAKE_TARGET_MODEL:-$MODEL}"
|
|
61
|
+
# FAKE_NGA_SURFACE=1 => retrieved AiAuthoringBundle contains connection messaging: + @utils.escalate
|
|
62
|
+
# (Service escalation surface present). 0 => neither (surface missing => INCOMPLETE on nga).
|
|
63
|
+
NGA_SURFACE="${FAKE_NGA_SURFACE:-1}"
|
|
64
|
+
RC_ID="0RC000000000001AAA"
|
|
65
|
+
# Opt-in call log: when FAKE_SF_CALLLOG points at a file, append every invocation's args so a
|
|
66
|
+
# test can prove which mutations did (or did NOT) happen - e.g. idempotent re-runs create nothing.
|
|
67
|
+
if [ -n "${FAKE_SF_CALLLOG:-}" ]; then printf '%s\n' "$args" >> "$FAKE_SF_CALLLOG"; fi
|
|
68
|
+
_has_members() { [ "$QUEUE_MEMBERS" = "1" ] || [ -f "$SELF_DIR/.gm_added" ]; }
|
|
69
|
+
case "$sub" in
|
|
70
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
71
|
+
"org list")
|
|
72
|
+
if printf '%s' "$args" | grep -q "metadata-types"; then
|
|
73
|
+
case "$MODEL" in
|
|
74
|
+
classic) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"GenAiPlugin"},{"xmlName":"GenAiPlannerBundle"}]}}';;
|
|
75
|
+
nga) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"AiAuthoringBundle"},{"xmlName":"GenAiPlugin"}]}}';;
|
|
76
|
+
both) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"AiAuthoringBundle"},{"xmlName":"GenAiPlugin"},{"xmlName":"GenAiPlannerBundle"}]}}';;
|
|
77
|
+
*) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"BotTemplate"},{"xmlName":"BotBlock"}]}}';;
|
|
78
|
+
esac
|
|
79
|
+
else
|
|
80
|
+
echo '{"status":0,"result":{}}'
|
|
81
|
+
fi
|
|
82
|
+
exit 0;;
|
|
83
|
+
"data query")
|
|
84
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
85
|
+
echo '{"status":0,"result":{"records":[{"Id":"00Dxx0000000000EAA","IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
|
|
86
|
+
elif printf '%s' "$args" | grep -q "FROM BotDefinition"; then
|
|
87
|
+
if [ "$AGENT_ACTIVE" = "1" ]; then
|
|
88
|
+
echo '{"status":0,"result":{"records":[{"Id":"0Xx000000000001AAA","DeveloperName":"Support_Agent","MasterLabel":"Support Agent","BotVersions":{"records":[{"Id":"04V000000000001AAA","Status":"Active"}]}}],"totalSize":1}}'
|
|
89
|
+
else
|
|
90
|
+
echo '{"status":0,"result":{"records":[{"Id":"0Xx000000000001AAA","DeveloperName":"Support_Agent","MasterLabel":"Support Agent","BotVersions":{"records":[{"Id":"04V000000000001AAA","Status":"Inactive"}]}}],"totalSize":1}}'
|
|
91
|
+
fi
|
|
92
|
+
elif printf '%s' "$args" | grep -q "FROM MessagingChannel"; then
|
|
93
|
+
if [ "$MC_PRESENT" = "1" ]; then
|
|
94
|
+
echo '{"status":0,"result":{"records":[{"Id":"0M9000000000001AAA","DeveloperName":"Support_Chat"}],"totalSize":1}}'
|
|
95
|
+
else
|
|
96
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
97
|
+
fi
|
|
98
|
+
elif printf '%s' "$args" | grep -q "FROM QueueSobject"; then
|
|
99
|
+
if [ "$QS_PRESENT" = "1" ]; then
|
|
100
|
+
echo '{"status":0,"result":{"records":[{"Id":"0Q9000000000001AAA"}],"totalSize":1}}'
|
|
101
|
+
else
|
|
102
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
103
|
+
fi
|
|
104
|
+
elif printf '%s' "$args" | grep -q "FROM QueueRoutingConfig"; then
|
|
105
|
+
if [ "$QRC_PRESENT" = "1" ]; then
|
|
106
|
+
echo '{"status":0,"result":{"records":[{"Id":"'"$RC_ID"'","DeveloperName":"Human_Support_Queue_RC"}],"totalSize":1}}'
|
|
107
|
+
else
|
|
108
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
109
|
+
fi
|
|
110
|
+
elif printf '%s' "$args" | grep -q "FROM FlowDefinitionView"; then
|
|
111
|
+
echo '{"status":0,"result":{"records":[{"ApiName":"Human_Escalation_Outbound_Flow","ActiveVersionId":"301000000000001AAA"}],"totalSize":1}}'
|
|
112
|
+
elif printf '%s' "$args" | grep -q "FROM GroupMember"; then
|
|
113
|
+
if _has_members; then
|
|
114
|
+
echo '{"status":0,"result":{"records":[{"UserOrGroupId":"005000000000001AAA"}],"totalSize":1}}'
|
|
115
|
+
else
|
|
116
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
117
|
+
fi
|
|
118
|
+
elif printf '%s' "$args" | grep -q "FROM User"; then
|
|
119
|
+
if printf '%s' "$args" | grep -q "COUNT(Id)"; then
|
|
120
|
+
if _has_members; then
|
|
121
|
+
echo '{"status":0,"result":{"records":[{"c":1}],"totalSize":1}}'
|
|
122
|
+
else
|
|
123
|
+
echo '{"status":0,"result":{"records":[{"c":0}],"totalSize":1}}'
|
|
124
|
+
fi
|
|
125
|
+
else
|
|
126
|
+
# Id-by-username (assignment path) → an active user row.
|
|
127
|
+
echo '{"status":0,"result":{"records":[{"Id":"005000000000002AAA"}],"totalSize":1}}'
|
|
128
|
+
fi
|
|
129
|
+
elif printf '%s' "$args" | grep -q "FROM Group"; then
|
|
130
|
+
if [ "$QUEUE_PRESENT" = "1" ]; then
|
|
131
|
+
if [ "$QUEUE_BOUND" = "1" ]; then
|
|
132
|
+
echo '{"status":0,"result":{"records":[{"Id":"00G000000000001AAA","QueueRoutingConfigId":"'"$RC_ID"'"}],"totalSize":1}}'
|
|
133
|
+
else
|
|
134
|
+
echo '{"status":0,"result":{"records":[{"Id":"00G000000000001AAA","QueueRoutingConfigId":null}],"totalSize":1}}'
|
|
135
|
+
fi
|
|
136
|
+
else
|
|
137
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
138
|
+
fi
|
|
139
|
+
else
|
|
140
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
141
|
+
fi
|
|
142
|
+
exit 0;;
|
|
143
|
+
"api request")
|
|
144
|
+
# Data API POST/PATCH (QueueSobject bind, QueueRoutingConfig create, Group.QueueRoutingConfigId bind).
|
|
145
|
+
if printf '%s' "$args" | grep -q "sobjects/QueueRoutingConfig"; then
|
|
146
|
+
echo '{"success":true,"id":"'"$RC_ID"'"}'
|
|
147
|
+
elif printf '%s' "$args" | grep -q "sobjects/QueueSobject"; then
|
|
148
|
+
echo '{"success":true,"id":"0Q9000000000009AAA"}'
|
|
149
|
+
elif printf '%s' "$args" | grep -q "sobjects/GroupMember"; then
|
|
150
|
+
touch "$SELF_DIR/.gm_added"
|
|
151
|
+
echo '{"success":true,"id":"0GM00000000001AAA"}'
|
|
152
|
+
else
|
|
153
|
+
# Group PATCH → 204 empty body on success.
|
|
154
|
+
echo ''
|
|
155
|
+
fi
|
|
156
|
+
exit 0;;
|
|
157
|
+
"project retrieve")
|
|
158
|
+
dir=$(printf '%s' "$args" | sed -n 's/.*--target-metadata-dir[ =]\([^ ]*\).*/\1/p'); dir="${dir:-.}"
|
|
159
|
+
mkdir -p "$dir"
|
|
160
|
+
if printf '%s' "$args" | grep -q "AiAuthoringBundle:"; then
|
|
161
|
+
[ "$TARGET_MODEL" = "nga" ] || [ "$TARGET_MODEL" = "both" ] || exit 1
|
|
162
|
+
if [ "$NGA_SURFACE" = "1" ]; then
|
|
163
|
+
cat > "$dir/Support_Agent.agent" <<'AGENT'
|
|
164
|
+
start_agent support_agent:
|
|
165
|
+
connection messaging:
|
|
166
|
+
end_user_client: customer_web_client
|
|
167
|
+
reasoning:
|
|
168
|
+
actions:
|
|
169
|
+
human_handoff: @utils.escalate
|
|
170
|
+
description: "Transfer the user to a human agent."
|
|
171
|
+
AGENT
|
|
172
|
+
else
|
|
173
|
+
cat > "$dir/Support_Agent.agent" <<'AGENT'
|
|
174
|
+
start_agent support_agent:
|
|
175
|
+
reasoning:
|
|
176
|
+
actions:
|
|
177
|
+
answer: @actions.AnswerQuestionWithKnowledge
|
|
178
|
+
AGENT
|
|
179
|
+
fi
|
|
180
|
+
elif printf '%s' "$args" | grep -q "GenAiPlugin:"; then
|
|
181
|
+
echo '<canEscalate>true</canEscalate>' > "$dir/plugin.xml"
|
|
182
|
+
elif printf '%s' "$args" | grep -q "GenAiPlannerBundle:"; then
|
|
183
|
+
[ "$TARGET_MODEL" = "classic" ] || [ "$TARGET_MODEL" = "both" ] || exit 1
|
|
184
|
+
cat > "$dir/bundle.xml" <<BXML
|
|
185
|
+
<GenAiPlannerBundle>
|
|
186
|
+
<plannerSurfaces>
|
|
187
|
+
<surfaceType>${ROUTE_SURFACE}</surfaceType>
|
|
188
|
+
<outboundRouteConfigs>
|
|
189
|
+
<escalationMessage>Transferring you to a live support agent.</escalationMessage>
|
|
190
|
+
<outboundRouteName>Human_Escalation_Outbound_Flow</outboundRouteName>
|
|
191
|
+
<outboundRouteType>${ROUTE_TYPE}</outboundRouteType>
|
|
192
|
+
</outboundRouteConfigs>
|
|
193
|
+
</plannerSurfaces>
|
|
194
|
+
</GenAiPlannerBundle>
|
|
195
|
+
BXML
|
|
196
|
+
fi
|
|
197
|
+
echo '{"status":0,"result":{"success":true}}'; exit 0;;
|
|
198
|
+
"project deploy")
|
|
199
|
+
echo '{"status":0,"result":{"success":true,"files":[{"fullName":"Human_Escalation_Outbound_Flow","type":"Flow","state":"Created"}]}}'; exit 0;;
|
|
200
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
201
|
+
esac
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
|
|
206
|
+
"""Run the orchestrator; return (returncode, merged stdout+stderr)."""
|
|
207
|
+
env = dict(os.environ)
|
|
208
|
+
if scrub_sf:
|
|
209
|
+
kept = [d for d in env.get("PATH", "").split(os.pathsep)
|
|
210
|
+
if d and not os.path.exists(os.path.join(d, "sf"))]
|
|
211
|
+
env["PATH"] = os.pathsep.join(kept)
|
|
212
|
+
if path_prefix:
|
|
213
|
+
env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
|
|
214
|
+
if extra_env:
|
|
215
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
216
|
+
proc = subprocess.run(
|
|
217
|
+
["bash", str(SKILLS_ROOT / SCRIPT), *args],
|
|
218
|
+
capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
|
|
219
|
+
)
|
|
220
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_node(script_rel, args):
|
|
224
|
+
"""Run a bundled Node helper; return (returncode, stdout)."""
|
|
225
|
+
proc = subprocess.run(
|
|
226
|
+
["node", str(SKILLS_ROOT / script_rel), *args],
|
|
227
|
+
capture_output=True, text=True, cwd=str(SKILLS_ROOT),
|
|
228
|
+
)
|
|
229
|
+
return proc.returncode, (proc.stdout or "")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class FakeSf:
|
|
233
|
+
"""Context manager: a temp dir holding an executable fake `sf` first on PATH."""
|
|
234
|
+
|
|
235
|
+
def __init__(self, body=_FAKE_SF_PROD):
|
|
236
|
+
self._body = body
|
|
237
|
+
self._tmp = None
|
|
238
|
+
|
|
239
|
+
def __enter__(self):
|
|
240
|
+
self._tmp = tempfile.mkdtemp(prefix="esc-fake-sf-")
|
|
241
|
+
sf = Path(self._tmp) / "sf"
|
|
242
|
+
sf.write_text(self._body)
|
|
243
|
+
sf.chmod(0o755)
|
|
244
|
+
return self._tmp
|
|
245
|
+
|
|
246
|
+
def __exit__(self, *exc):
|
|
247
|
+
import shutil
|
|
248
|
+
if self._tmp:
|
|
249
|
+
shutil.rmtree(self._tmp, ignore_errors=True)
|