@salesforce/afv-skills 1.59.0 → 1.60.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/agentforce-architecture-analyze/SKILL.md +66 -17
- package/skills/agentforce-architecture-analyze/assets/cli/show_access_token.yaml +4 -7
- package/skills/agentforce-architecture-analyze/references/contract.json +1 -1
- package/skills/agentforce-architecture-analyze/scripts/emit_env.py +2 -2
- package/skills/agentforce-architecture-analyze/scripts/main.py +40 -44
- package/skills/agentforce-architecture-analyze/scripts/rest_client.py +11 -3
- package/skills/agentforce-architecture-analyze/scripts/sf_cli.py +77 -10
- package/skills/agentforce-architecture-analyze/scripts/tests/test_end_to_end_fixture.py +1 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_flow_children_inflation.py +1 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_main_pipeline.py +53 -2
- package/skills/agentforce-architecture-analyze/scripts/tests/test_resolve_creds.py +184 -36
- package/skills/agentforce-architecture-analyze/scripts/tests/test_rest_client.py +17 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_sf_cli.py +165 -11
- package/skills/agentforce-architecture-analyze/scripts/tests/test_skill_md_pipeline_block.py +319 -0
- package/skills/agentforce-d360-analyze/SKILL.md +28 -10
- package/skills/agentforce-d360-analyze/scripts/dc.py +200 -50
- package/skills/agentforce-d360-analyze/scripts/discover_sessions.py +10 -6
- package/skills/agentforce-d360-analyze/scripts/fetch_dc.py +22 -6
- package/skills/agentforce-d360-analyze/scripts/resolve_session.py +21 -13
- package/skills/agentforce-d360-analyze/scripts/tests/test_dc_and_resolve_session.py +298 -76
- package/skills/agentforce-d360-analyze/scripts/tests/test_default_target_org.py +432 -0
- package/skills/agentforce-d360-analyze/scripts/tests/test_discover_sessions.py +1 -1
- package/skills/agentforce-d360-analyze/scripts/tests/test_resolve_session_main.py +9 -3
- package/skills/agentforce-d360-analyze/scripts/tests/test_skill_md_prefix_block.py +176 -0
- package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +17 -27
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +7 -17
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +47 -63
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +10 -13
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -46
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +4 -21
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +58 -82
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +16 -315
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +9 -24
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -44
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +11 -63
- package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +2 -1
- package/skills/data360-schema-get/SKILL.md +3 -3
- package/skills/design-systems-slds-validate/references/report-format.md +1 -1
- package/skills/dx-org-manage/SKILL.md +1 -0
- package/skills/dx-org-shape-manage/SKILL.md +1 -0
- package/skills/dx-org-snapshot-manage/SKILL.md +172 -0
- package/skills/dx-org-snapshot-manage/examples/create_error_output.json +16 -0
- package/skills/dx-org-snapshot-manage/examples/create_success_output.json +19 -0
- package/skills/dx-org-snapshot-manage/examples/delete_output.json +9 -0
- package/skills/dx-org-snapshot-manage/examples/get_output.json +19 -0
- package/skills/dx-org-snapshot-manage/examples/list_output.json +36 -0
- package/skills/dx-org-snapshot-manage/references/cli_flags.md +179 -0
- package/skills/dx-org-snapshot-manage/references/snapshot_usage.md +55 -0
- package/skills/dx-org-trial-expiration-check/SKILL.md +1 -1
- package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +2 -2
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +7 -5
- package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +11 -11
- package/skills/experience-content-media-image-generate/SKILL.md +83 -0
- package/skills/experience-content-media-image-generate/scripts/download-image.sh +84 -0
- package/skills/external-diagram-mermaid-generate/SKILL.md +12 -12
- package/skills/field-service-data-capture-migrate/references/component-mapping.md +1 -1
- package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +1 -2
- package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +1 -0
- package/skills/field-service-data-capture-migrate/references/transformer-rules.md +1 -1
- package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +12 -1
- package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +4 -1
- package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -1
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +233 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/accounting-setup.md +117 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/billing-rules-config.md +237 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/billing-settings.md +271 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/demo-policy-config.md +138 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/org-defaults.md +64 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/permissions-config.md +226 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/prerequisites.md +142 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +3 -2
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +3 -3
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +68 -45
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +17 -18
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +16 -3
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +16 -13
- package/skills/marketing-message-test/SKILL.md +194 -0
- package/skills/marketing-message-test/references/api-contract.md +333 -0
- package/skills/omnistudio-callable-apex-generate/SKILL.md +3 -3
- package/skills/omnistudio-datamapper-generate/SKILL.md +6 -6
- package/skills/omnistudio-datapacks-deploy/SKILL.md +11 -11
- package/skills/omnistudio-dependencies-analyze/SKILL.md +5 -5
- package/skills/omnistudio-epc-catalog-generate/SKILL.md +6 -6
- package/skills/omnistudio-flexcard-generate/SKILL.md +6 -6
- package/skills/omnistudio-integration-procedure-generate/SKILL.md +6 -6
- package/skills/omnistudio-omniscript-generate/SKILL.md +6 -5
- package/skills/platform-apex-logs-debug/SKILL.md +1 -0
- package/skills/platform-metadata-deploy/SKILL.md +1 -0
- package/skills/platform-models-api-configure/SKILL.md +5 -5
- package/skills/platform-soql-query/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +17 -6
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +107 -192
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +6 -6
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +49 -20
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +4 -4
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +224 -290
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +295 -0
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -1
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +2 -1
- package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +2 -2
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +20 -13
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +5 -55
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +0 -38
- package/skills/service-omni-command-center-analyze/SKILL.md +2 -3
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
- package/skills/tableau-next-asset-create/README.md +65 -0
- package/skills/tableau-next-asset-create/SKILL.md +409 -0
- package/skills/tableau-next-asset-create/references/admin-users.md +149 -0
- package/skills/tableau-next-asset-create/references/ai-readiness.md +355 -0
- package/skills/tableau-next-asset-create/references/alerts.md +111 -0
- package/skills/tableau-next-asset-create/references/build-workflow.md +819 -0
- package/skills/tableau-next-asset-create/references/dashboard-authoring.md +302 -0
- package/skills/tableau-next-asset-create/references/dashboard-design-principles.md +352 -0
- package/skills/tableau-next-asset-create/references/data-transform-gotchas.md +210 -0
- package/skills/tableau-next-asset-create/references/data-understanding.md +387 -0
- package/skills/tableau-next-asset-create/references/deep-reference-index.md +74 -0
- package/skills/tableau-next-asset-create/references/destructive-operations.md +62 -0
- package/skills/tableau-next-asset-create/references/edit-dashboard.md +273 -0
- package/skills/tableau-next-asset-create/references/edit-visualization.md +529 -0
- package/skills/tableau-next-asset-create/references/empty-source-handling.md +104 -0
- package/skills/tableau-next-asset-create/references/ingest-and-metric-gotchas.md +638 -0
- package/skills/tableau-next-asset-create/references/insight-bundle.md +69 -0
- package/skills/tableau-next-asset-create/references/large-flat-file-handling.md +331 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_envelope.json +54 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_shared-widget-fields.json +122 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/add_page.json +29 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/delete_page.json +27 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/discard_dashboard.json +11 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget_to_page.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/place_widget_on_page.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_dashboard.json +16 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_page.json +26 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/rename_page.json +28 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/save_dashboard.json +18 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/undo.json +11 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_button_widget.json +69 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_container_widget.json +88 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_filter_widget.json +146 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_metric_widget.json +78 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_navigation_widget.json +80 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_parameter_widget.json +160 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_text_widget.json +103 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_visualization_widget.json +68 -0
- package/skills/tableau-next-asset-create/references/sdm-tool-reference.md +721 -0
- package/skills/tableau-next-asset-create/references/semantic-query-and-enrichment.md +355 -0
- package/skills/tableau-next-asset-create/references/shared-gates.md +322 -0
- package/skills/tableau-next-asset-create/references/sharing-and-promotion.md +306 -0
- package/skills/tableau-next-asset-create/references/tasks/ai-readiness-audit.md +51 -0
- package/skills/tableau-next-asset-create/references/tasks/analyze-data.md +48 -0
- package/skills/tableau-next-asset-create/references/tasks/build-dashboard.md +66 -0
- package/skills/tableau-next-asset-create/references/tasks/build-end-to-end.md +73 -0
- package/skills/tableau-next-asset-create/references/tasks/create-data-transform.md +51 -0
- package/skills/tableau-next-asset-create/references/tasks/create-viz.md +58 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-dashboard.md +80 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-sdm.md +106 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-viz.md +40 -0
- package/skills/tableau-next-asset-create/references/tasks/enrich-model.md +68 -0
- package/skills/tableau-next-asset-create/references/tasks/ingest-flat-file.md +68 -0
- package/skills/tableau-next-asset-create/references/tasks/manage-alert.md +35 -0
- package/skills/tableau-next-asset-create/references/tasks/profile-dataset.md +47 -0
- package/skills/tableau-next-asset-create/references/tasks/promote-or-reuse.md +46 -0
- package/skills/tableau-next-asset-create/references/tasks/provision-user.md +39 -0
- package/skills/tableau-next-asset-create/references/tasks/query-model.md +92 -0
- package/skills/tableau-next-asset-create/references/tasks/review-verified-questions.md +42 -0
- package/skills/tableau-next-asset-create/references/tasks/share-asset.md +37 -0
- package/skills/tableau-next-asset-create/references/verified-questions.md +125 -0
- package/skills/tableau-next-asset-create/references/viz-authoring.md +992 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +0 -102
package/package.json
CHANGED
|
@@ -22,9 +22,23 @@ Design-time metadata tree for one Agentforce agent: planner → topics → actio
|
|
|
22
22
|
|
|
23
23
|
Runs **inline** — no subagent. Every phase is deterministic file processing.
|
|
24
24
|
|
|
25
|
-
## If the user hasn't
|
|
25
|
+
## If the user hasn't named an agent
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Trigger condition: `$ARGUMENTS` names no agent (no `--agent` flag and no exact `BotDefinition` API name in the prose). Never invent an agent name. Instead, discover what the org actually contains — one read-only query, no pipeline yet. Use the `--org` alias if given, otherwise the default target org (omit `--target-org`):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sf data query --query "SELECT DeveloperName, MasterLabel FROM BotDefinition ORDER BY DeveloperName" \
|
|
31
|
+
[--target-org <alias>] --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the user gave no org alias, the pipeline block defaults `--org` to the CLI default target org itself (`sf config get target-org --json` → `.result[0].value`) — you don't need to resolve it.
|
|
35
|
+
|
|
36
|
+
Then branch on the rows returned:
|
|
37
|
+
|
|
38
|
+
- **Zero agents** — tell the user the org has no Agentforce agents and stop. Do not fabricate an agent or a tree.
|
|
39
|
+
- **Exactly one agent, or exactly one whose `DeveloperName`/`MasterLabel` matches the user's description** (e.g. "support", "service", "sales") — use it, say explicitly which agent you picked and why, then run the pipeline block below with `ARG_AGENT=<DeveloperName>` (and `ARG_ORG=<alias>` if the user named one) set as its first lines, e.g. `ARG_AGENT=Customer_Support_Agent`. The block reads `ARG_*` from the environment; flags in `$ARGUMENTS` still override.
|
|
40
|
+
- **Otherwise** — print the block below, with the discovered agents listed as a numbered `DeveloperName — MasterLabel` table under the first line, and wait for the user to pick.
|
|
41
|
+
- **Query fails** (no default org, auth error) — print the block below without the list.
|
|
28
42
|
|
|
29
43
|
> Which agent should I document, and in which org?
|
|
30
44
|
>
|
|
@@ -41,7 +55,7 @@ When invoked with no `agent_api_name` AND no org alias, print the following bloc
|
|
|
41
55
|
|
|
42
56
|
## Pipeline invocation
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
Run this block once the agent is known — from `--agent <api_name>` in `$ARGUMENTS`, or preset as `ARG_AGENT=<DeveloperName>` after discovery. `--org` (or `ARG_ORG`) is optional; without it the CLI default target org is used. One `python3` invocation drives the full pipeline. `main.py` writes `.emit_ctx.json`; `emit_result.py` reads it and prints the final `=== RESULT ===` block last to stdout.
|
|
45
59
|
|
|
46
60
|
```bash
|
|
47
61
|
set -euo pipefail
|
|
@@ -53,20 +67,44 @@ set -euo pipefail
|
|
|
53
67
|
# matching the bash shebang's expectation. No-op under bash.
|
|
54
68
|
[ -n "${ZSH_VERSION:-}" ] && setopt KSH_ARRAYS
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
# <SKILL_DIR> = absolute path of this skill's own directory (the folder holding
|
|
71
|
+
# this SKILL.md); substitute it from the skill path in context. Runtimes install
|
|
72
|
+
# skills in different places, so also probe the known install roots and use the
|
|
73
|
+
# first that actually contains this skill's scripts. An exported SKILL_ROOT wins.
|
|
74
|
+
_skill=agentforce-architecture-analyze
|
|
75
|
+
for _c in "${SKILL_ROOT:-}" "<SKILL_DIR>" \
|
|
76
|
+
"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/$_skill}" \
|
|
77
|
+
"${VIBES_SKILLS_DIR:+$VIBES_SKILLS_DIR/$_skill}" \
|
|
78
|
+
"$HOME/.claude/skills/$_skill" "$PWD/.claude/skills/$_skill" \
|
|
79
|
+
"${PLUGIN_ROOT:-$HOME/.vibe/skills}/$_skill"; do
|
|
80
|
+
[ -n "$_c" ] || continue
|
|
81
|
+
# Some stagers (ADK eval) nest the bundled files under <skill>/artifacts/.
|
|
82
|
+
for _r in "$_c" "$_c/artifacts"; do
|
|
83
|
+
[ -f "$_r/scripts/main.py" ] && { SKILL_ROOT="$_r"; break 2; }
|
|
84
|
+
done
|
|
85
|
+
done
|
|
86
|
+
[ -f "${SKILL_ROOT:-}/scripts/main.py" ] || { echo "$_skill: scripts not found — set SKILL_ROOT to this skill's directory" >&2; exit 1; }
|
|
57
87
|
|
|
58
88
|
# Argument parser. Accepts both `--org foo` and `--org=foo`.
|
|
59
|
-
# `$ARGUMENTS` is the raw user input Claude Code substitutes
|
|
60
|
-
|
|
61
|
-
ARG_AGENT=
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
# `$ARGUMENTS` is the raw user input Claude Code substitutes; it may be unset
|
|
90
|
+
# or plain prose. Each ARG_* starts from the environment, so a value resolved
|
|
91
|
+
# during discovery can be preset (e.g. `ARG_AGENT=MyAgent` on the first line);
|
|
92
|
+
# flags in $ARGUMENTS still override.
|
|
93
|
+
ARG_ORG="${ARG_ORG:-}"
|
|
94
|
+
ARG_AGENT="${ARG_AGENT:-}"
|
|
95
|
+
ARG_VERSION="${ARG_VERSION:-}"
|
|
96
|
+
ARG_FORCE="${ARG_FORCE:-}"
|
|
97
|
+
ARG_REPROBE="${ARG_REPROBE:-}"
|
|
98
|
+
ARG_PARALLELISM="${ARG_PARALLELISM:-}"
|
|
99
|
+
ARG_MAX_MERMAID="${ARG_MAX_MERMAID:-}"
|
|
100
|
+
|
|
101
|
+
# Word-split $ARGUMENTS (zsh needs SH_WORD_SPLIT for that) with globbing off
|
|
102
|
+
# so prose like `*` never expands to filenames.
|
|
103
|
+
[ -n "${ZSH_VERSION:-}" ] && setopt SH_WORD_SPLIT
|
|
104
|
+
set -f
|
|
68
105
|
# shellcheck disable=SC2206
|
|
69
|
-
_args=($ARGUMENTS)
|
|
106
|
+
_args=(${ARGUMENTS:-})
|
|
107
|
+
set +f
|
|
70
108
|
i=0
|
|
71
109
|
while [ $i -lt ${#_args[@]} ]; do
|
|
72
110
|
tok="${_args[$i]}"
|
|
@@ -87,6 +125,16 @@ while [ $i -lt ${#_args[@]} ]; do
|
|
|
87
125
|
i=$((i+1))
|
|
88
126
|
done
|
|
89
127
|
|
|
128
|
+
# No org named anywhere: fall back to the sf CLI default target org
|
|
129
|
+
# (`sf config get target-org --json` -> .result[0].value). Extracted with sed
|
|
130
|
+
# so the block needs neither jq nor an inline interpreter; any failure leaves
|
|
131
|
+
# ARG_ORG empty and the usage block below fires.
|
|
132
|
+
if [ -z "$ARG_ORG" ] && command -v sf >/dev/null 2>&1; then
|
|
133
|
+
_cfg="$(sf config get target-org --json 2>/dev/null || true)"
|
|
134
|
+
_cfg="$(printf '%s\n' "$_cfg" | sed -n 's/.*"value"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' || true)"
|
|
135
|
+
ARG_ORG="${_cfg%%$'\n'*}"
|
|
136
|
+
fi
|
|
137
|
+
|
|
90
138
|
# Usage block if required flags missing. Agent reads stderr,
|
|
91
139
|
# prints verbatim, and stops — does NOT pre-run main.py.
|
|
92
140
|
if [ -z "$ARG_ORG" ] || [ -z "$ARG_AGENT" ]; then
|
|
@@ -95,7 +143,8 @@ if [ -z "$ARG_ORG" ] || [ -z "$ARG_AGENT" ]; then
|
|
|
95
143
|
>
|
|
96
144
|
> I need:
|
|
97
145
|
> - **Agent API name** — the BotDefinition.DeveloperName (e.g. `MyAgent`)
|
|
98
|
-
> - **Org alias** — for `sf` CLI auth (the alias you configured with `sf org login`)
|
|
146
|
+
> - **Org alias** — for `sf` CLI auth (the alias you configured with `sf org login`);
|
|
147
|
+
> only needed when no default target org is set (`sf config set target-org <alias>`)
|
|
99
148
|
>
|
|
100
149
|
> Optional flags:
|
|
101
150
|
> - `--version v5` — pin a specific BotVersion (default: Active+highest)
|
|
@@ -152,7 +201,7 @@ exit "$_rc"
|
|
|
152
201
|
|
|
153
202
|
| Input | Flag | Required | Default |
|
|
154
203
|
|---|---|---|---|
|
|
155
|
-
| `org_alias` | `--org` |
|
|
204
|
+
| `org_alias` | `--org` | no | sf CLI default target org (`sf config get target-org`) |
|
|
156
205
|
| `agent_api_name` | `--agent` | yes | — |
|
|
157
206
|
| `agent_version_api_name` | `--version` | no | active BotVersion |
|
|
158
207
|
| `force_refresh` | `--force` | no | false (honor cache) |
|
|
@@ -219,7 +268,7 @@ The ID-prefix router in `resolve_invocation_target.py` distinguishes the two: NG
|
|
|
219
268
|
|
|
220
269
|
| Tool | Required |
|
|
221
270
|
|---|---|
|
|
222
|
-
| `sf` CLI (authenticated against the target org) | yes — `sf org login web --alias <alias
|
|
271
|
+
| `sf` CLI (authenticated against the target org) | yes — `sf org login web --alias <alias>`, and the CLI must provide `sf org auth show-access-token` (startup preflight enforces this) |
|
|
223
272
|
| Python 3.10+ | yes |
|
|
224
273
|
|
|
225
274
|
## Reference docs to load when needed
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
name: show_access_token
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# command is the long-term replacement.
|
|
2
|
+
# Access-token retrieval per forcedotcom/cli#3560. The `sf org display --json`
|
|
3
|
+
# accessToken field is redacted to a placeholder string; the dedicated
|
|
4
|
+
# `sf org auth show-access-token` command is the replacement.
|
|
6
5
|
#
|
|
7
6
|
# `--no-prompt` skips the interactive confirmation banner so the command
|
|
8
7
|
# behaves identically under `--json`.
|
|
9
8
|
#
|
|
10
|
-
#
|
|
11
|
-
# stays wired for sf CLI versions that don't ship show-access-token; that
|
|
12
|
-
# fallback is orchestrated in `main.py::_resolve_creds`, not here.
|
|
9
|
+
# This command is the only supported runtime token-retrieval path.
|
|
13
10
|
argv:
|
|
14
11
|
- sf
|
|
15
12
|
- org
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"required": {
|
|
7
7
|
"org_alias": {
|
|
8
8
|
"type": "string",
|
|
9
|
-
"description": "sf CLI alias (e.g. 'my-org-alias'). Auth is always derived via `sf org display --target-org $ORG_ALIAS --json
|
|
9
|
+
"description": "sf CLI alias (e.g. 'my-org-alias'). Auth is always derived via `sf org display --target-org $ORG_ALIAS --json` (instance URL) and `sf org auth show-access-token --target-org $ORG_ALIAS --json --no-prompt` (access token)."
|
|
10
10
|
},
|
|
11
11
|
"agent_api_name": {
|
|
12
12
|
"type": "string",
|
|
@@ -24,9 +24,9 @@ Any path component is a dictionary key OR an integer array index
|
|
|
24
24
|
is the empty string — caller decides whether that's OK.
|
|
25
25
|
|
|
26
26
|
Usage:
|
|
27
|
-
# Parse `sf org display --json` output via stdin
|
|
27
|
+
# Parse `sf org display --json` output via stdin (org metadata only —
|
|
28
|
+
# access tokens come only from `sf org auth show-access-token --json --no-prompt`)
|
|
28
29
|
eval "$(printf '%s' "$_SF_JSON" | python3 emit_env.py stdin \\
|
|
29
|
-
'ACCESS_TOKEN:result.accessToken' \\
|
|
30
30
|
'INSTANCE_URL:result.instanceUrl' \\
|
|
31
31
|
'ORG_ID_18:result.id' \\
|
|
32
32
|
'ORG_ID_15:result.id[:15]')"
|
|
@@ -46,7 +46,12 @@ from config import (
|
|
|
46
46
|
build_agent_data_dir,
|
|
47
47
|
)
|
|
48
48
|
from rest_client import RestClientError, redact_error
|
|
49
|
-
from sf_cli import
|
|
49
|
+
from sf_cli import (
|
|
50
|
+
AuthRequired,
|
|
51
|
+
SfCliError,
|
|
52
|
+
assert_show_access_token_capability,
|
|
53
|
+
run_sf,
|
|
54
|
+
)
|
|
50
55
|
from soql_loader import SoqlParamError
|
|
51
56
|
|
|
52
57
|
# Phase 2 Batch 1 imports — functional pipeline primitives.
|
|
@@ -180,9 +185,12 @@ def _build_creds_plumbing(
|
|
|
180
185
|
on every call so a refresh actually lands in the NEXT SOQL attempt.
|
|
181
186
|
`refresh_fn` is serialized by an internal `threading.Lock` + a
|
|
182
187
|
`time.monotonic()` dedupe window — N concurrent 401s against the same
|
|
183
|
-
stale token collapse to ONE `resolve_creds()`
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
stale token collapse to ONE `resolve_creds()` call per window. The
|
|
189
|
+
window is stamped when `resolve_creds()` RETURNS, so waiters queued
|
|
190
|
+
behind a slow refresh (several sf CLI spawns: `sf org display` +
|
|
191
|
+
`sf org auth show-access-token`) reuse its result instead of
|
|
192
|
+
re-refreshing. That caps full credential refreshes at roughly one per
|
|
193
|
+
window regardless of pool size.
|
|
186
194
|
|
|
187
195
|
Extracted from `main()` so the closure is testable without threading
|
|
188
196
|
a whole pipeline run through argparse + mocks.
|
|
@@ -205,7 +213,9 @@ def _build_creds_plumbing(
|
|
|
205
213
|
# Another thread refreshed within the dedupe window — reuse.
|
|
206
214
|
return creds_cell[0]
|
|
207
215
|
creds_cell[0] = resolve_creds()
|
|
208
|
-
|
|
216
|
+
# Stamp AFTER the (possibly slow) refresh so threads that queued
|
|
217
|
+
# on the lock during it fall inside the window and reuse it.
|
|
218
|
+
last_refresh_mono[0] = time.monotonic()
|
|
209
219
|
return creds_cell[0]
|
|
210
220
|
|
|
211
221
|
return creds_provider, refresh_fn, creds_cell
|
|
@@ -222,25 +232,25 @@ been stable since cli#3560 landed."""
|
|
|
222
232
|
def _resolve_creds(org_alias: str) -> Tuple[str, str]:
|
|
223
233
|
"""Resolve (instance_url, access_token) for ``org_alias``.
|
|
224
234
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
2. **Fallback** — if the dedicated command isn't shipped in the
|
|
232
|
-
installed sf CLI version (older releases), fall back to the
|
|
233
|
-
legacy ``sf org display --verbose`` with the
|
|
234
|
-
``SF_TEMP_SHOW_SECRETS=true`` env var that ``run_sf`` injects
|
|
235
|
-
unconditionally. The fallback is detected by the redaction
|
|
236
|
-
placeholder appearing in the ``accessToken`` field — exact match
|
|
237
|
-
on the substring ``show-access-token`` (see
|
|
238
|
-
``_REDACTION_MARKER_FRAGMENT``).
|
|
235
|
+
Runtime token retrieval is fail-fast (forcedotcom/cli#3560): the
|
|
236
|
+
access token comes ONLY from ``sf org auth show-access-token``.
|
|
237
|
+
``sf org display`` is still used for non-secret metadata
|
|
238
|
+
(instanceUrl). A missing command, an empty token, or the redaction
|
|
239
|
+
placeholder (see ``_REDACTION_MARKER_FRAGMENT``) all raise
|
|
240
|
+
``AuthRequired``.
|
|
239
241
|
|
|
240
242
|
sf_cli.run_sf already redacts stderr on failure paths. Any
|
|
241
243
|
exception surfaces as `AuthRequired` / `SfCliError`; caller routes
|
|
242
244
|
through `_emit_fail`.
|
|
243
245
|
"""
|
|
246
|
+
# Capability preflight first (cached once it passes, see sf_cli) so a
|
|
247
|
+
# missing sf binary / command surfaces the actionable preflight error
|
|
248
|
+
# rather than a generic spawn failure from `sf org display`.
|
|
249
|
+
try:
|
|
250
|
+
assert_show_access_token_capability()
|
|
251
|
+
except SfCliError as exc:
|
|
252
|
+
raise AuthRequired(str(exc)) from exc
|
|
253
|
+
|
|
244
254
|
display_data = run_sf("org_display", ORG_ALIAS=org_alias)
|
|
245
255
|
display_result = display_data.get("result") or {}
|
|
246
256
|
instance_url = display_result.get("instanceUrl") or ""
|
|
@@ -249,37 +259,23 @@ def _resolve_creds(org_alias: str) -> Tuple[str, str]:
|
|
|
249
259
|
"sf org display returned an incomplete payload (missing instanceUrl)"
|
|
250
260
|
)
|
|
251
261
|
|
|
252
|
-
# Primary path: dedicated command. Catches both the unknown-command
|
|
253
|
-
# case (older sf CLI) and any AuthRequired surfaced from the recipe.
|
|
254
262
|
try:
|
|
255
263
|
token_data = run_sf("show_access_token", ORG_ALIAS=org_alias)
|
|
256
|
-
except SfCliError:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
# Defensive fallback: if the dedicated command silently returned an
|
|
267
|
-
# empty token OR sf CLI redacted the org_display token to the
|
|
268
|
-
# placeholder, try the org_display payload directly. SF_TEMP_SHOW_SECRETS
|
|
269
|
-
# is set unconditionally by run_sf so this works as long as the env
|
|
270
|
-
# var is still honoured by the installed sf CLI.
|
|
271
|
-
if not access_token or _REDACTION_MARKER_FRAGMENT in access_token:
|
|
272
|
-
legacy_token = display_result.get("accessToken") or ""
|
|
273
|
-
if legacy_token and _REDACTION_MARKER_FRAGMENT not in legacy_token:
|
|
274
|
-
access_token = legacy_token
|
|
264
|
+
except (SfCliError, AuthRequired) as exc:
|
|
265
|
+
raise AuthRequired(
|
|
266
|
+
"could not retrieve access token via sf org auth "
|
|
267
|
+
"show-access-token; ensure sf CLI is logged in for this org "
|
|
268
|
+
f"and supports the dedicated command. Detail: {exc}"
|
|
269
|
+
) from exc
|
|
270
|
+
|
|
271
|
+
token_result = token_data.get("result") or {}
|
|
272
|
+
access_token = token_result.get("accessToken") or ""
|
|
275
273
|
|
|
276
274
|
if not access_token or _REDACTION_MARKER_FRAGMENT in access_token:
|
|
277
275
|
raise AuthRequired(
|
|
278
276
|
"could not retrieve a usable access token via sf org auth "
|
|
279
|
-
"show-access-token
|
|
280
|
-
"
|
|
281
|
-
"dedicated command is available or SF_TEMP_SHOW_SECRETS is "
|
|
282
|
-
"still honoured."
|
|
277
|
+
"show-access-token; ensure sf CLI is logged in for this org "
|
|
278
|
+
"and the command returned a real token."
|
|
283
279
|
)
|
|
284
280
|
|
|
285
281
|
return instance_url, access_token
|
|
@@ -46,12 +46,14 @@ class RestClientError(RuntimeError):
|
|
|
46
46
|
# -----------------------------------------------------------------------------
|
|
47
47
|
# error redaction
|
|
48
48
|
# -----------------------------------------------------------------------------
|
|
49
|
-
#
|
|
49
|
+
# Four patterns cover the bulk of token-leakage surfaces:
|
|
50
50
|
# 1. `Authorization: Bearer <token>` in any string context (header echo,
|
|
51
51
|
# exception repr, stringified HTTP error).
|
|
52
|
-
# 2. `
|
|
52
|
+
# 2. A bare `Bearer <token>` with no `Authorization:` prefix (curl-style
|
|
53
|
+
# traces, CLI stderr, hand-built header dicts).
|
|
54
|
+
# 3. `accessToken=<token>` / `access_token=<token>` in URL-encoded bodies
|
|
53
55
|
# or query strings (`sf org display` errors sometimes echo these).
|
|
54
|
-
#
|
|
56
|
+
# 4. `"accessToken":"<token>"` or `"access_token":"<token>"` in JSON
|
|
55
57
|
# payload echoes.
|
|
56
58
|
#
|
|
57
59
|
# Regexes are intentionally permissive on the token character class
|
|
@@ -62,6 +64,11 @@ _AUTH_HEADER_RE = re.compile(
|
|
|
62
64
|
r"(Authorization\s*:\s*Bearer\s+)\S+",
|
|
63
65
|
flags=re.IGNORECASE,
|
|
64
66
|
)
|
|
67
|
+
# Matches a bare `Bearer <value>`; stops at whitespace or a quote.
|
|
68
|
+
_BEARER_RE = re.compile(
|
|
69
|
+
r"(\bBearer\s+)[^\s\"']+",
|
|
70
|
+
flags=re.IGNORECASE,
|
|
71
|
+
)
|
|
65
72
|
# Matches access[_]?Token=<value> in url-encoded form; stops at & or whitespace.
|
|
66
73
|
_ACCESS_TOKEN_QS_RE = re.compile(
|
|
67
74
|
r"(access[_]?token\s*=\s*)[^&\s\"']+",
|
|
@@ -91,6 +98,7 @@ def redact_text(text: str) -> str:
|
|
|
91
98
|
if not text:
|
|
92
99
|
return text
|
|
93
100
|
text = _AUTH_HEADER_RE.sub(r"\1<redacted>", text)
|
|
101
|
+
text = _BEARER_RE.sub(r"\1<redacted>", text)
|
|
94
102
|
text = _ACCESS_TOKEN_QS_RE.sub(r"\1<redacted>", text)
|
|
95
103
|
text = _ACCESS_TOKEN_JSON_RE.sub(r'\1<redacted>', text)
|
|
96
104
|
return text
|
|
@@ -18,7 +18,6 @@ into logs or RESULT blocks.
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
|
-
import os
|
|
22
21
|
import re
|
|
23
22
|
import subprocess
|
|
24
23
|
from pathlib import Path
|
|
@@ -57,6 +56,83 @@ class SfCliError(RuntimeError):
|
|
|
57
56
|
"""
|
|
58
57
|
|
|
59
58
|
|
|
59
|
+
_show_access_token_capability_ok = False
|
|
60
|
+
"""Set once the preflight succeeds; the check then never reruns in this
|
|
61
|
+
process (401-triggered creds refreshes skip it). Failures are NOT cached,
|
|
62
|
+
so a fixed PATH/upgrade is picked up on the next call."""
|
|
63
|
+
|
|
64
|
+
_SHOW_ACCESS_TOKEN_HELP_SIGNATURE_RE = re.compile(
|
|
65
|
+
r"\$\s*sf\s+org\s+auth\s+show-access-token\b"
|
|
66
|
+
)
|
|
67
|
+
"""USAGE-line form that real ``--help`` output always contains
|
|
68
|
+
(``$ sf org auth show-access-token -o <value> ...``). The bare command name
|
|
69
|
+
is not enough: unknown-command notices echo it too
|
|
70
|
+
(``Command org:auth:show-access-token not found``,
|
|
71
|
+
``show-access-token is not a sf command``). A zero-exit probe whose output
|
|
72
|
+
lacks the USAGE form is treated as a failed check."""
|
|
73
|
+
|
|
74
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def assert_show_access_token_capability() -> None:
|
|
78
|
+
"""Fail fast when the installed sf CLI lacks ``sf org auth show-access-token``.
|
|
79
|
+
|
|
80
|
+
That command is the only supported runtime token-retrieval path
|
|
81
|
+
(forcedotcom/cli#3560). Probing ``--help`` is cheap, needs no org auth,
|
|
82
|
+
and gives the operator an actionable upgrade message instead of a
|
|
83
|
+
confusing downstream auth failure. A successful probe is cached for
|
|
84
|
+
the process lifetime.
|
|
85
|
+
|
|
86
|
+
Exit status alone is not trusted: a CLI build that answers an unknown
|
|
87
|
+
command with exit 0 (e.g. printing top-level help or a "not found"
|
|
88
|
+
notice) would otherwise pass. The probe therefore also requires the
|
|
89
|
+
command's USAGE line (``$ sf org auth show-access-token``) in the
|
|
90
|
+
ANSI-stripped stdout+stderr; if it is absent the check fails and is not
|
|
91
|
+
cached.
|
|
92
|
+
"""
|
|
93
|
+
global _show_access_token_capability_ok
|
|
94
|
+
if _show_access_token_capability_ok:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
proc = subprocess.run(
|
|
98
|
+
["sf", "org", "auth", "show-access-token", "--help"],
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=30,
|
|
102
|
+
check=True,
|
|
103
|
+
)
|
|
104
|
+
except FileNotFoundError as exc:
|
|
105
|
+
raise SfCliError(
|
|
106
|
+
"sf CLI not found on PATH — install Salesforce CLI first"
|
|
107
|
+
) from exc
|
|
108
|
+
except subprocess.CalledProcessError as exc:
|
|
109
|
+
safe_detail = _redact_subprocess_stderr(exc.stderr or exc.stdout or "")
|
|
110
|
+
raise SfCliError(
|
|
111
|
+
"sf CLI is missing required command 'sf org auth show-access-token'. "
|
|
112
|
+
"Upgrade/reinstall Salesforce CLI to a version that provides this "
|
|
113
|
+
f"command. Preflight detail: {safe_detail or 'unknown error'}"
|
|
114
|
+
) from exc
|
|
115
|
+
except subprocess.TimeoutExpired as exc:
|
|
116
|
+
raise SfCliError(
|
|
117
|
+
f"sf CLI capability preflight timed out: {redact_error(exc)}"
|
|
118
|
+
) from exc
|
|
119
|
+
help_text = _ANSI_ESCAPE_RE.sub(
|
|
120
|
+
"", f"{proc.stdout or ''}\n{proc.stderr or ''}",
|
|
121
|
+
)
|
|
122
|
+
if not _SHOW_ACCESS_TOKEN_HELP_SIGNATURE_RE.search(help_text):
|
|
123
|
+
# Zero exit without the command's help signature — the CLI did not
|
|
124
|
+
# recognise the command. Fail (uncached) like a non-zero exit.
|
|
125
|
+
# Redact before truncating so a token can never be split past the regex.
|
|
126
|
+
safe_detail = _redact_subprocess_stderr(help_text.strip())[:500]
|
|
127
|
+
raise SfCliError(
|
|
128
|
+
"sf CLI is missing required command 'sf org auth show-access-token'. "
|
|
129
|
+
"Upgrade/reinstall Salesforce CLI to a version that provides this "
|
|
130
|
+
"command. Preflight detail: "
|
|
131
|
+
f"{safe_detail or 'help output lacked the command signature'}"
|
|
132
|
+
)
|
|
133
|
+
_show_access_token_capability_ok = True
|
|
134
|
+
|
|
135
|
+
|
|
60
136
|
def _load_recipe(name: str) -> Dict[str, Any]:
|
|
61
137
|
"""Read and parse a CLI recipe file with yaml.safe_load.
|
|
62
138
|
|
|
@@ -175,14 +251,6 @@ def run_sf(name: str, **params: str) -> Dict[str, Any]:
|
|
|
175
251
|
timeout = int(recipe.get("timeout_seconds", 60))
|
|
176
252
|
auth_patterns = recipe.get("auth_required_stderr_patterns") or []
|
|
177
253
|
|
|
178
|
-
# SF_TEMP_SHOW_SECRETS=true is required for `sf org display --verbose
|
|
179
|
-
# --json` to emit `accessToken` instead of the literal redaction string
|
|
180
|
-
# `"[REDACTED] Use 'sf org auth show-access-token' to view"` introduced
|
|
181
|
-
# in sf CLI v2. Without it, downstream callers receive the redaction
|
|
182
|
-
# string as the bearer token and every Tooling/REST call returns
|
|
183
|
-
# INVALID_AUTH_HEADER 401. Set on every recipe — recipes that don't
|
|
184
|
-
# touch tokens are unaffected.
|
|
185
|
-
env = {**os.environ, "SF_TEMP_SHOW_SECRETS": "true"}
|
|
186
254
|
try:
|
|
187
255
|
cp = subprocess.run(
|
|
188
256
|
argv,
|
|
@@ -190,7 +258,6 @@ def run_sf(name: str, **params: str) -> Dict[str, Any]:
|
|
|
190
258
|
text=True,
|
|
191
259
|
timeout=timeout,
|
|
192
260
|
check=False,
|
|
193
|
-
env=env,
|
|
194
261
|
)
|
|
195
262
|
except subprocess.TimeoutExpired as e:
|
|
196
263
|
# timeout exceptions carry stderr via e.stderr — redact.
|
|
@@ -87,6 +87,7 @@ def _mock_auth_probe(probe_result=None):
|
|
|
87
87
|
}
|
|
88
88
|
return [
|
|
89
89
|
mock.patch.object(main, "run_sf", return_value=org_display_payload),
|
|
90
|
+
mock.patch.object(main, "assert_show_access_token_capability"),
|
|
90
91
|
mock.patch.object(main, "probe_channels", return_value=probe_result),
|
|
91
92
|
]
|
|
92
93
|
|
package/skills/agentforce-architecture-analyze/scripts/tests/test_flow_children_inflation.py
CHANGED
|
@@ -244,6 +244,7 @@ def _mock_auth_probe():
|
|
|
244
244
|
}
|
|
245
245
|
return [
|
|
246
246
|
mock.patch.object(main, "run_sf", return_value=org_display_payload),
|
|
247
|
+
mock.patch.object(main, "assert_show_access_token_capability"),
|
|
247
248
|
mock.patch.object(main, "probe_channels", return_value=fx.probe_ok_payload()),
|
|
248
249
|
]
|
|
249
250
|
|
|
@@ -131,6 +131,7 @@ def _mock_auth_probe(probe_result=None):
|
|
|
131
131
|
org_display_payload["result"]["id"] = "00Dxx0000000000AAA"
|
|
132
132
|
return [
|
|
133
133
|
mock.patch.object(main, "run_sf", return_value=org_display_payload),
|
|
134
|
+
mock.patch.object(main, "assert_show_access_token_capability"),
|
|
134
135
|
mock.patch.object(main, "probe_channels", return_value=probe_result),
|
|
135
136
|
]
|
|
136
137
|
|
|
@@ -852,11 +853,13 @@ class RenderFailureIntegrationTests(unittest.TestCase):
|
|
|
852
853
|
|
|
853
854
|
# Drive emit_result against this ctx and confirm the RESULT
|
|
854
855
|
# block reflects the render failure.
|
|
855
|
-
|
|
856
|
+
# emit_result.py ships under scripts/ (SKILL.md invokes
|
|
857
|
+
# "$SKILL_ROOT/scripts/emit_result.py"), not the hub-era tools/.
|
|
858
|
+
scripts_dir = Path(__file__).resolve().parent.parent
|
|
856
859
|
import subprocess, sys as _sys
|
|
857
860
|
env = {**os.environ, "WORK_DIR": str(work_dir)}
|
|
858
861
|
r = subprocess.run(
|
|
859
|
-
[_sys.executable, str(
|
|
862
|
+
[_sys.executable, str(scripts_dir / "emit_result.py")],
|
|
860
863
|
env=env, capture_output=True, text=True, timeout=30,
|
|
861
864
|
)
|
|
862
865
|
self.assertEqual(r.returncode, 0, msg=r.stderr)
|
|
@@ -1012,6 +1015,53 @@ class ThreadSafeRefreshTests(unittest.TestCase):
|
|
|
1012
1015
|
|
|
1013
1016
|
self.assertEqual(call_count["n"], 2)
|
|
1014
1017
|
|
|
1018
|
+
def test_slow_refresh_waiters_do_not_re_refresh(self):
|
|
1019
|
+
"""A refresh that takes LONGER than the dedupe window must still
|
|
1020
|
+
satisfy threads that queued on the lock while it ran. The window
|
|
1021
|
+
is stamped when `resolve_creds()` returns, not when it starts —
|
|
1022
|
+
otherwise every queued waiter re-refreshes after a slow
|
|
1023
|
+
multi-command resolve.
|
|
1024
|
+
"""
|
|
1025
|
+
import threading as _t
|
|
1026
|
+
import time as _time
|
|
1027
|
+
|
|
1028
|
+
calls = {"n": 0}
|
|
1029
|
+
entered = _t.Event()
|
|
1030
|
+
|
|
1031
|
+
def slow_resolve():
|
|
1032
|
+
calls["n"] += 1
|
|
1033
|
+
entered.set()
|
|
1034
|
+
_time.sleep(0.3) # longer than the 0.1s window below
|
|
1035
|
+
return ("url", f"tok_{calls['n']}")
|
|
1036
|
+
|
|
1037
|
+
_provider, refresh, _cell = main._build_creds_plumbing(
|
|
1038
|
+
("url", "tok_0"),
|
|
1039
|
+
resolve_creds=slow_resolve,
|
|
1040
|
+
dedupe_window_s=0.1,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
results: list = []
|
|
1044
|
+
results_lock = _t.Lock()
|
|
1045
|
+
|
|
1046
|
+
def _worker():
|
|
1047
|
+
r = refresh()
|
|
1048
|
+
with results_lock:
|
|
1049
|
+
results.append(r)
|
|
1050
|
+
|
|
1051
|
+
first = _t.Thread(target=_worker)
|
|
1052
|
+
first.start()
|
|
1053
|
+
self.assertTrue(entered.wait(timeout=5))
|
|
1054
|
+
# These all block on the refresh lock while slow_resolve runs.
|
|
1055
|
+
waiters = [_t.Thread(target=_worker) for _ in range(4)]
|
|
1056
|
+
for t in waiters:
|
|
1057
|
+
t.start()
|
|
1058
|
+
first.join()
|
|
1059
|
+
for t in waiters:
|
|
1060
|
+
t.join()
|
|
1061
|
+
|
|
1062
|
+
self.assertEqual(calls["n"], 1)
|
|
1063
|
+
self.assertEqual(results, [("url", "tok_1")] * 5)
|
|
1064
|
+
|
|
1015
1065
|
def test_provider_reads_cell_after_refresh(self):
|
|
1016
1066
|
"""After refresh mutates the cell, the NEXT `creds_provider()`
|
|
1017
1067
|
call returns the fresh tuple (the contract).
|
|
@@ -1512,6 +1562,7 @@ class ApiVersionEndToEndTests(unittest.TestCase):
|
|
|
1512
1562
|
]
|
|
1513
1563
|
patches = [
|
|
1514
1564
|
mock.patch.object(main, "run_sf", return_value=org_display_payload),
|
|
1565
|
+
mock.patch.object(main, "assert_show_access_token_capability"),
|
|
1515
1566
|
mock.patch.object(main, "probe_channels",
|
|
1516
1567
|
return_value=fx.probe_ok_payload()),
|
|
1517
1568
|
*_mock_bot_resolution(),
|