Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
486 changes: 437 additions & 49 deletions solutions/ess-maker-skills/scripts/flightcheck/checks/publishing.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
(never assert a verdict from an unconfirmed API response shape) — this
checkpoint echoes the observed ``connectionParametersSet.name`` for the
operator to confirm, rather than PASS/FAIL on a guessed value.
* ``DV-CONN-001`` (S5.4) — the Dataverse connection reference the extension
pack ships (``…_92b66``, connector ``shared_commondataserviceforapps``) is
bound to an **active** connection, and its owner is echoed so the operator
can confirm it is their **own** account. Programmatic PASS/FAIL on a
documented-tier Dataverse ``connectionreferences`` read.
* ``DV-CONN-001`` (S5.4) — the Workday SOAP connection reference reported by
the Declarative Agent minimalBots components API is bound (``connectionId``
present), and its owner is echoed so the operator can confirm it is their
**own** account. Programmatic PASS/FAIL on the validated minimalBots
components read (same endpoint + ``connectionReferenceChanges`` shape as the
shipped native ``DA-CONN-001`` check).
* ``WD-REST-001`` (S5.5) — the captured ``restBaseUrl`` is present and
**trimmed to** ``/api``. Pure-config check, no client.
* ``WD-REST-002`` (S5.7) — the agent's ``user-context-setup.mcs.yml`` topic
Expand All @@ -43,7 +44,7 @@
whole run.
* **One CheckResult per checkpoint** (principle 7).
* **No guessed API shapes** — the two API-backed checks read documented fields
only (Dataverse ``connectionid`` / ``statuscode``; BAP
only (minimalBots ``connectionReferenceChanges`` connector/connection ids; BAP
``connectionParametersSet.name`` / ``createdBy``), and degrade gracefully
when a client is unavailable.
* **Every** ``CheckResult`` declares ``roles=`` (enforced by
Expand All @@ -52,19 +53,12 @@

from __future__ import annotations

import os
import re
import sys
from pathlib import Path

from ..runner import CheckResult, Priority, Role, Status
from ..agent_scope import resolve_agent_directory, validate_agent_slug

# scripts/auth.py is on sys.path via cli.py at runtime (tests add it too); this
# mirrors checks/environment.py's top-level import so query_all is patchable as
# flightcheck.checks.workday_extension.query_all.
from auth import query_all # noqa: E402

DOC_BASE = (
"https://learn.microsoft.com/en-us/copilot/microsoft-365/"
"employee-self-service"
Expand Down Expand Up @@ -92,12 +86,9 @@
_WORKDAY_RUNTIME_REF_LOGICAL_NAME = (
"msdyn_sharedworkdaysoap_workdayruntime"
)
# The Dataverse connection reference the simplified pack ships.
_DATAVERSE_CONNECTOR_SUFFIX = "/apis/shared_commondataserviceforapps"
_DATAVERSE_REF_SUFFIX = "92b66"
_DATAVERSE_RUNTIME_REF_LOGICAL_NAME = (
"msdyn_sharedcommondataserviceforapps_workdayruntime"
)
# The Workday SOAP connection reference the Declarative Agent reports via the
# minimalBots components API (connector ``shared_workdaysoap``).
_WORKDAY_CONNECTOR_SUFFIX = "/apis/shared_workdaysoap"
_REF_SUFFIX_RE = re.compile(r"_([0-9a-f]{5})$")

# ---- Local user-context topic (WD-REST-002) ----
Expand All @@ -110,7 +101,7 @@
"Workday connection authentication type is Microsoft Entra ID Integrated"
)
_DV_CONN_DESC = (
"Dataverse connection reference bound to an active connection you own"
"Workday SOAP connection reference bound to a connection you own"
)
_REST_URL_DESC = "Workday REST base URL present and trimmed to '/api'"
_REDIRECT_DESC = (
Expand Down Expand Up @@ -194,14 +185,6 @@ def _is_workday_auth_ref(logical_name) -> bool:
)


def _is_dataverse_runtime_ref(logical_name) -> bool:
normalized = str(logical_name or "").casefold()
return (
_ref_suffix(logical_name) == _DATAVERSE_REF_SUFFIX
or normalized == _DATAVERSE_RUNTIME_REF_LOGICAL_NAME.casefold()
)


def _host_of(url: str) -> str:
"""Return the host portion of an ``https://host/…`` URL for display."""
match = re.match(r"https?://([^/]+)", str(url).strip())
Expand Down Expand Up @@ -231,26 +214,48 @@ def _resolve_owner(props: dict) -> str:


def _query_connection_references(runner):
"""Return all Dataverse ``connectionreferences`` rows, or ``None`` when the
Dataverse token/endpoint is not available.

Documented-tier read (Dataverse Web API v9.2) — no cassette required; tests
stub ``query_all``.
"""Return the agent's connection references from the Declarative Agent
minimalBots components API, normalized to the row shape
``_check_dv_connection`` consumes, or ``None`` when the AgentBuilder client
or the active-agent ``botId`` is unavailable.

Validated-tier read (minimalBots ``POST …/components``). The same endpoint
and ``connectionReferenceChanges`` shape already back the shipped native
``DA-CONN-001`` check (``checks/native_agent.py``); see
``tests/fixtures/cassettes/INDEX.md`` and ``tests/mocks/
agentbuilder_connectivity.py``. Fails loudly (lets the dispatcher degrade
this checkpoint to a WARNING) rather than overclaiming: an
``AgentBuilderHTTPError`` propagates, and a 200 payload whose
``connectionReferenceChanges`` is present but not a list raises
``ValueError`` (mirrors ``native_agent._connection_references``). A missing
changeset is treated as "no references" (genuine absence), not an error.
"""
env_url = getattr(runner, "env_url", None)
dv_token = getattr(runner, "dv_token", None)
if not env_url or not dv_token:
client = getattr(runner, "agentbuilder", None)
config = getattr(runner, "config", None) or {}
agent_id = (config.get("agent") or {}).get("botId")
if client is None or not agent_id:
return None
# Belt-and-suspenders: keep scripts/ importable even if the module was
# imported before cli.py put it on the path.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
return query_all(
env_url,
dv_token,
"connectionreferences",
"connectionreferenceid,connectionreferencelogicalname,"
"connectionreferencedisplayname,connectorid,connectionid,statuscode",
)
changeset = client.fetch_components(agent_id) or {}
changes = changeset.get("connectionReferenceChanges")
if changes is None:
return []
if not isinstance(changes, list):
raise ValueError(
"Component fetch returned invalid connectionReferenceChanges."
)
refs = []
for change in changes:
ref = (change or {}).get("connectionReference") or {}
refs.append(
{
"connectionreferencelogicalname": ref.get(
"connectionReferenceLogicalName"
),
"connectorid": ref.get("connectorId"),
"connectionid": ref.get("connectionId"),
}
)
return refs


def _get_connections(runner):
Expand Down Expand Up @@ -405,7 +410,7 @@ def _check_connection_auth(runner) -> list[CheckResult]:


# ─────────────────────────────────────────────────────────────────────
# DV-CONN-001 — Dataverse connection reference binding (S5.4, PASS/FAIL).
# DV-CONN-001 — Workday SOAP connection reference binding (S5.4, PASS/FAIL).
# ─────────────────────────────────────────────────────────────────────


Expand All @@ -417,99 +422,58 @@ def _check_dv_connection(runner) -> list[CheckResult]:
priority=Priority.HIGH.value, status=Status.SKIPPED.value,
description=_DV_CONN_DESC,
result=(
"Dataverse token not available — skipping the Dataverse "
"connection-reference check."
"AgentBuilder client or active-agent botId not available — "
"skipping the Workday connection-reference check."
),
)]

dv_refs = [
r
for r in refs
if str(r.get("connectorid") or "").lower().endswith(
_DATAVERSE_CONNECTOR_SUFFIX
)
and _is_dataverse_runtime_ref(
r.get("connectionreferencelogicalname")
)
]
if len(dv_refs) > 1:
names = ", ".join(
sorted(
str(ref.get("connectionreferencelogicalname") or "(unnamed)")
for ref in dv_refs
)
)
return [CheckResult(roles=_MAKER_ROLES,
checkpoint_id="DV-CONN-001", category=_CATEGORY,
priority=Priority.HIGH.value, status=Status.WARNING.value,
description=_DV_CONN_DESC,
result=(
"Multiple ESS Dataverse connection references match the "
f"runtime and legacy package fingerprints: {names}. "
"FlightCheck cannot determine which reference is active."
),
remediation=(
"Remove obsolete Workday package references, then rerun "
"FlightCheck against the remaining Dataverse binding."
),
doc_link=_DOC_SIMPLIFIED,
)]
dv_ref = dv_refs[0] if dv_refs else None
wd_ref = next(
(
r
for r in refs
if str(r.get("connectorid") or "")
.lower()
.endswith(_WORKDAY_CONNECTOR_SUFFIX)
),
None,
)

if dv_ref is None:
if wd_ref is None:
return [CheckResult(roles=_MAKER_ROLES,
checkpoint_id="DV-CONN-001", category=_CATEGORY,
priority=Priority.HIGH.value, status=Status.NOT_CONFIGURED.value,
priority=Priority.HIGH.value, status=Status.FAILED.value,
description=_DV_CONN_DESC,
result=(
"The ESS Dataverse connection reference "
f"(\u2026_{_DATAVERSE_REF_SUFFIX}, connector "
"shared_commondataserviceforapps) was not found in this "
"environment."
"The ESS Workday SOAP connection reference (connector "
"shared_workdaysoap) was not found in the Declarative Agent "
"components payload."
),
remediation=(
"Install/repair the Workday extension pack so its Dataverse "
"connection reference is created, then bind it to a Dataverse "
"connection you own."
"Install or repair the Workday extension pack so its Workday "
"SOAP connection reference is created, then bind it to a "
"Workday connection you own."
),
doc_link=_DOC_SIMPLIFIED,
)]

dv_ref_name = str(dv_ref.get("connectionreferencelogicalname"))
connection_id = dv_ref.get("connectionid")
statuscode = dv_ref.get("statuscode")
wd_ref_name = str(
wd_ref.get("connectionreferencelogicalname") or "(unnamed)"
)
connection_id = wd_ref.get("connectionid")

if not connection_id:
return [CheckResult(roles=_MAKER_ROLES,
checkpoint_id="DV-CONN-001", category=_CATEGORY,
priority=Priority.HIGH.value, status=Status.FAILED.value,
description=_DV_CONN_DESC,
result=(
"The ESS Dataverse connection reference "
f"({dv_ref_name}) is unbound "
"(connectionid=null)."
),
remediation=(
"In Power Platform / Copilot Studio, bind the Dataverse "
"connection reference to an active Dataverse connection owned "
"by your own account."
),
doc_link=_DOC_SIMPLIFIED,
)]

if statuscode != 1:
return [CheckResult(roles=_MAKER_ROLES,
checkpoint_id="DV-CONN-001", category=_CATEGORY,
priority=Priority.HIGH.value, status=Status.FAILED.value,
description=_DV_CONN_DESC,
result=(
"The ESS Dataverse connection reference "
f"({dv_ref_name}) is bound but inactive "
f"(statuscode={statuscode})."
"The ESS Workday SOAP connection reference "
f"({wd_ref_name}) is unbound (connectionId=null)."
),
remediation=(
"Re-authenticate or re-bind the Dataverse connection so its "
"status is active, using an account you own."
"In Power Platform / Copilot Studio, bind the Workday SOAP "
"connection reference to an active Workday connection owned by "
"your own account."
),
doc_link=_DOC_SIMPLIFIED,
)]
Expand All @@ -529,9 +493,8 @@ def _check_dv_connection(runner) -> list[CheckResult]:
priority=Priority.HIGH.value, status=Status.PASSED.value,
description=_DV_CONN_DESC,
result=(
"The ESS Dataverse connection reference "
f"({dv_ref_name}) is bound to an active "
"connection." + owner_note
"The ESS Workday SOAP connection reference "
f"({wd_ref_name}) is bound to a connection." + owner_note
),
doc_link=_DOC_SIMPLIFIED,
)]
Expand Down
9 changes: 8 additions & 1 deletion solutions/ess-maker-skills/scripts/flightcheck/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@
("Native Agent", run_native_agent_checks),
("Environment", run_capacity_check),
("Local Files", run_local_file_checks),
("Publishing", run_publishing_checks),
],
"environment": [("Environment", run_capacity_check)],
"servicenow": [("Native Agent", run_native_agent_checks)],
"workday": [("Native Agent", run_native_agent_checks)],
"publishing": [("Publishing", run_publishing_checks)],
}
NATIVE_CONNECTOR_FILTERS = {
"servicenow": ("shared_service-now",),
Expand Down Expand Up @@ -1399,7 +1401,12 @@ def main():
)
sys.exit(1)

needs_agent_readiness = args.scope in {"full", "servicenow", "workday"}
needs_agent_readiness = args.scope in {
"full",
"servicenow",
"workday",
"publishing",
}
if needs_agent_readiness:
environment_host = str(
config.get("powerPlatformApiEndpoint") or ""
Expand Down
31 changes: 27 additions & 4 deletions solutions/ess-maker-skills/scripts/flightcheck/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
run_preferred_solution_check,
)
from flightcheck.checks.native_agent import run_native_agent_checks
from flightcheck.checks.publishing import run_publishing_checks
from flightcheck.checks.external_systems import run_external_systems_checks
from flightcheck.checks.solution import run_solution_checks
from flightcheck.checks.workday import run_workday_checks
Expand Down Expand Up @@ -238,6 +239,26 @@ class ResolvedPlan:
),
is_family=True,
),
CheckpointSpec(
key="PUB-001",
category_fn=run_publishing_checks,
category_label="Publishing",
clients=frozenset({AGENTBUILDER}),
requires_config=True,
requires_dataverse_endpoint=False,
priority=Priority.CRITICAL.value,
roles=(Role.ESS_MAKER.value,),
),
CheckpointSpec(
key="PUB-002",
category_fn=run_publishing_checks,
category_label="Publishing",
clients=frozenset({AGENTBUILDER}),
requires_config=True,
requires_dataverse_endpoint=False,
priority=Priority.CRITICAL.value,
roles=(Role.ESS_MAKER.value, Role.POWER_PLATFORM_ADMIN.value),
),
CheckpointSpec(
key="ENV-009",
category_fn=run_preferred_solution_check,
Expand Down Expand Up @@ -540,15 +561,16 @@ class ResolvedPlan:
priority=Priority.HIGH.value,
roles=(Role.ESS_MAKER.value,),
),
# DV-CONN-001 — self-contained Dataverse read (its own connectionreferences
# query) plus a best-effort BAP owner echo.
# DV-CONN-001 — reads the Workday SOAP connection reference from the
# Declarative Agent minimalBots components API (AGENTBUILDER), plus a
# best-effort BAP owner echo (PP_ADMIN).
CheckpointSpec(
key="DV-CONN-001",
category_fn=run_workday_extension_checks,
category_label="Workday Extension",
clients=frozenset({DATAVERSE, PP_ADMIN}),
clients=frozenset({AGENTBUILDER, PP_ADMIN}),
requires_config=True,
requires_dataverse_endpoint=True,
requires_dataverse_endpoint=False,
priority=Priority.HIGH.value,
roles=(Role.ESS_MAKER.value,),
),
Expand Down Expand Up @@ -664,6 +686,7 @@ class ResolvedPlan:
"WD-REST",
"WD-NET",
"DV-CONN",
"PUB",
"TOPIC-TRIGGER",
"TOPIC-INTEGRATION",
)
Expand Down
Loading
Loading