Skip to content
Merged
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
23 changes: 13 additions & 10 deletions solutions/ess-maker-skills/.github/prompts/setup.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ Read `src/skills/foundation-setup/SKILL.md` first. Follow its **Command runtime*
instructions to establish a working Python invocation before running any Python
command.

After reading the foundation skill, write the complete maker-facing progress
checklist below. At the beginning of every subsequent setup turn, write the
same complete checklist again using the latest canonical setup state and
results observed in that invocation. Use the exact ordinary Markdown shape
After reading the foundation skill, use its explicit progress render points.
At the first interactive setup surface in a turn, write the complete
maker-facing progress checklist below using the latest canonical setup state
and results observed in that invocation. Use the exact ordinary Markdown shape
defined in the foundation skill: one single-level bullet and one leading
status emoji per stage.

Expand All @@ -23,12 +23,11 @@ status emoji per stage.
- {marker} Review the setup handoff

Use ✅ for completed, 🔄 for the current stage, ⛔ for a blocked stage, and ⬜
for pending. Every update is a full snapshot containing all five stages in this
order. After each setup action that changes progress, write the complete
snapshot with the updated statuses. Preserve completed stages, keep pending
stages present, and represent subordinate checks through the status of their
owning stage. Before every maker-facing response, including the final handoff,
synchronize the complete snapshot once more.
for pending. Every rendered update is a full snapshot containing all five
stages in this order. Render it at the first interactive surface in a turn,
when a marker changes, when a blocked state requires maker action, and in the
final handoff. A sequence of setup operations that retains the same markers
continues to its next render point without another progress snapshot.

Run setup commands from the current ESS Maker Skills workspace folder.

Expand Down Expand Up @@ -56,6 +55,10 @@ If the check fails, run:

Then rerun the check.

After successful runtime, dependency, and converter checks, run the next setup
operation. When a check requires maker action, state the observed failure and
its single recovery action.

For any command failure, follow the **Command runtime** recovery guidance.

Do not route to Dataverse foundation or onboarding playbooks.
Expand Down
41 changes: 22 additions & 19 deletions solutions/ess-maker-skills/scripts/agentbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

CLIENT_ID = "417219b4-3a7d-42a2-bdb1-972bd8281a02"
DEFAULT_API_VERSION = "2024-10-01"
NATIVE_ALM_API_VERSION = "2022-03-01-preview"
COPILOT_STUDIO_CLIENT_NAME = "CopilotStudio"
DEFAULT_TOKEN_CACHE = Path(".local/.agentbuilder_token_cache.bin")
DEV_REALM = 0
TEST_REALM = 1
Expand Down Expand Up @@ -248,7 +250,7 @@ def list_environments(
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"x-ms-client-name": "EssAdk",
"x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME,
}
url = f"{host}/environmentmanagement/environments"
params: dict[str, str] | None = {"api-version": api_version}
Expand Down Expand Up @@ -585,7 +587,7 @@ def __init__(
self.headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"x-ms-client-name": "EssAdk",
"x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME,
}

def list_connections(self, environment_id: str) -> list[dict[str, Any]]:
Expand Down Expand Up @@ -651,8 +653,7 @@ def __init__(
self.headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"x-ms-client-name": "EssAdk",
"x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME,
}

def _json(
Expand All @@ -668,11 +669,14 @@ def _json(
request_params = {"api-version": self.api_version}
if params:
request_params.update(params)
request_headers = dict(self.headers)
if body is not None:
request_headers["Content-Type"] = "application/json"
response = self.session.request(
method,
f"{self.host}{path}",
params=request_params,
headers=self.headers,
headers=request_headers,
json=body,
timeout=timeout,
)
Expand Down Expand Up @@ -773,8 +777,8 @@ def publish_agent(
response = self.session.request(
"POST",
f"{self.host}/copilotstudio/minimalBots/api/{agent_id}/publish",
params={"api-version": self.api_version},
headers=self.headers,
params={"api-version": NATIVE_ALM_API_VERSION},
headers={**self.headers, "Content-Type": "application/json"},
json={},
timeout=timeout,
allow_redirects=False,
Expand Down Expand Up @@ -835,6 +839,7 @@ def fetch_components(self, agent_id: str) -> dict[str, Any]:
"POST",
f"/copilotstudio/minimalBots/api/{agent_id}/components",
"Component fetch",
params={"api-version": NATIVE_ALM_API_VERSION},
body={},
timeout=180,
)
Expand All @@ -855,8 +860,8 @@ def update_bot_entity(
return self.session.request(
"PUT",
f"{self.host}/copilotstudio/minimalBots/api/{agent_id}/components",
params={"api-version": self.api_version},
headers=self.headers,
params={"api-version": NATIVE_ALM_API_VERSION},
headers={**self.headers, "Content-Type": "application/json"},
json={"bot": bot, "botComponentChanges": []},
timeout=timeout,
allow_redirects=False,
Expand All @@ -871,9 +876,8 @@ def import_package(
) -> dict[str, Any]:
"""Import one package without replaying or exposing its response body."""
request_headers = {
name: value
for name, value in self.headers.items()
if name.casefold() != "content-type"
"Authorization": self.headers["Authorization"],
"x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME,
}
form = (
{"schemaName": replacement_schema_name}
Expand All @@ -884,11 +888,11 @@ def import_package(
response = self.session.request(
"POST",
f"{self.host}/copilotstudio/minimalBots/alm/import",
params={"api-version": self.api_version},
params={"api-version": NATIVE_ALM_API_VERSION},
headers=request_headers,
files={
"package": (
package_path.name,
"package.zip",
package,
"application/zip",
)
Expand Down Expand Up @@ -941,14 +945,13 @@ def export_package(
) -> None:
"""Export one native package to a caller-owned path."""
request_headers = {
name: value
for name, value in self.headers.items()
if name.casefold() != "content-type"
"Authorization": self.headers["Authorization"],
"x-ms-client-name": COPILOT_STUDIO_CLIENT_NAME,
}
response = self.session.request(
"POST",
f"{self.host}/copilotstudio/minimalBots/alm/{agent_id}/export",
params={"api-version": self.api_version},
params={"api-version": NATIVE_ALM_API_VERSION},
headers=request_headers,
timeout=timeout,
allow_redirects=False,
Expand Down Expand Up @@ -1007,7 +1010,7 @@ def create_agent_from_starter_package(
"POST",
f"{self.host}/copilotstudio/minimalBots/createFromStarterPackage",
params={"api-version": self.api_version},
headers=self.headers,
headers={**self.headers, "Content-Type": "application/json"},
json={"packageId": package_id},
timeout=timeout,
allow_redirects=False,
Expand Down
6 changes: 5 additions & 1 deletion solutions/ess-maker-skills/scripts/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,11 @@ def _publish_native(
print(f" ❌ Publish failed: {error}")
return 1

if result.get("validationPending") is True:
validation_pending = result.get(
"ValidationPending",
result.get("validationPending"),
)
if validation_pending is True:
print(
" ✅ Publish request accepted. Copilot Studio validation is "
"still running."
Expand Down
57 changes: 38 additions & 19 deletions solutions/ess-maker-skills/scripts/setup_existing_da.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,8 @@ def _confirm_dev_route(
realms: dict[str, Any],
*,
expected_schema_name: str | None,
) -> str:
allow_missing_schema: bool = False,
) -> str | None:
route_realm = realms.get("routeRealm")
if route_realm not in (0, "dev", "Dev"):
raise ExistingDASetupError(
Expand All @@ -1198,6 +1199,8 @@ def _confirm_dev_route(
expected_schema = str(expected_schema_name or "").strip()
schema_name = str(agent.get("schemaName") or expected_schema).strip()
if not schema_name:
if allow_missing_schema:
return None
raise ExistingDASetupError(
"Direct agent lookup did not return a schema name."
)
Expand All @@ -1220,6 +1223,7 @@ def validate_existing_dev_connection(
setup_source: str = "existing-dev",
require_alm_family: bool = True,
expected_schema_name: str | None = None,
allow_missing_schema: bool = False,
) -> dict[str, Any]:
"""Validate a directly addressable agent as editable Dev identity."""
normalized_setup_source = _validate_setup_source(setup_source)
Expand Down Expand Up @@ -1248,13 +1252,15 @@ def validate_existing_dev_connection(
agent,
realms,
expected_schema_name=expected_schema_name,
allow_missing_schema=allow_missing_schema,
)
family_id = None
agent_name = str(
agent.get("fullBotName")
or agent.get("displayName")
or agent.get("shortBotName")
or schema_name
or normalized_agent_id
)
managed_properties = agent.get("managedProperties")
is_managed = (
Expand Down Expand Up @@ -1404,7 +1410,7 @@ def _validate_changeset_identity(
agent_id: str,
*,
expected_schema_name: str | None = None,
) -> None:
) -> str:
bot = changeset.get("bot")
if not isinstance(bot, dict):
raise ExistingDASetupError("Component fetch did not return bot identity.")
Expand All @@ -1416,17 +1422,20 @@ def _validate_changeset_identity(
raise ExistingDASetupError(
"Component fetch returned content for a different agent."
)
fetched_schema = str(bot.get("schemaName") or "").strip()
if not fetched_schema:
raise ExistingDASetupError(
"Component fetch did not return a schema name."
)
expected_schema = str(expected_schema_name or "").strip()
if expected_schema:
fetched_schema = str(bot.get("schemaName") or "").strip()
if not fetched_schema:
raise ExistingDASetupError(
"Component fetch did not return the imported schema name."
)
if fetched_schema.casefold() != expected_schema.casefold():
raise ExistingDASetupError(
"Component fetch returned content for a different schema."
)
if (
expected_schema
and fetched_schema.casefold() != expected_schema.casefold()
):
raise ExistingDASetupError(
"Component fetch returned content for a different schema."
)
return fetched_schema


def _materialize_workspace(
Expand Down Expand Up @@ -1922,9 +1931,19 @@ def attach_existing_dev(
agent_id=agent_id,
selection_source=selection_source,
setup_source=setup_source,
require_alm_family=setup_source != "alm-import",
require_alm_family=False,
expected_schema_name=expected_schema_name,
allow_missing_schema=True,
)
normalized_environment_id = connection["environment"]["id"]
normalized_agent_id = connection["agent"]["id"]
prefetched_changeset: dict[str, Any] | None = None
if not connection["agent"]["schemaName"]:
prefetched_changeset = client.fetch_components(normalized_agent_id)
connection["agent"]["schemaName"] = _validate_changeset_identity(
prefetched_changeset,
normalized_agent_id,
)
canonical_state, existing_setup = _validate_setup_target(
kit_root,
connection,
Expand Down Expand Up @@ -1962,19 +1981,19 @@ def attach_existing_dev(
existing_setup,
)
progress_recorded = True
normalized_environment_id = connection["environment"]["id"]
normalized_agent_id = connection["agent"]["id"]
schema_name = connection["agent"]["schemaName"]
family_id = connection["agent"].get("almFamilyId")
agent_name = connection["agent"]["name"]
try:
changeset = client.fetch_components(normalized_agent_id)
changeset = (
prefetched_changeset
if prefetched_changeset is not None
else client.fetch_components(normalized_agent_id)
)
_validate_changeset_identity(
changeset,
normalized_agent_id,
expected_schema_name=(
schema_name if setup_source == "alm-import" else None
),
expected_schema_name=expected_schema_name or schema_name,
)
except Exception as exc:
if progress_recorded:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This reference defines the safety and evidence contract for installing a new Dev
| A created starter agent is not automatically opted into ALM | Live-proven |
| ALM opt-in uses the full fetched `BotEntity`, adds `configuration.settings["alm.isAlmEnabled"] = true`, and submits an empty `botComponentChanges` list through `PUT /api/{agentId}/components` | Live-proven |
| ALM opt-in persisted and advanced the BotEntity version on read-back | Live-proven |
| Direct Dev-route and component validation can materialize a newly created starter agent while published Dev configuration is absent | Live-proven in TEST |
| Existing-Dev validation and attachment remain separate and expose service-owned prerequisites | Live-proven |
| Direct native ALM import already creates agents from packages declaring `packageType: "templated"` | Live-proven, using the generic import path, not this surface |

Expand Down Expand Up @@ -52,8 +53,8 @@ Do not describe a pending-validation claim as supported behavior. In particular,
9. End create after emitting a usable returned identity. Preserve catalog revision as `catalogPackageVersion` and the service-returned source template version as `templateVersion`; never conflate them.
10. Invoke ALM enablement only after a successful create response in the confirmed setup flow. Fetch the exact agent first, deep-copy and preserve its full `BotEntity`, change only `alm.isAlmEnabled`, and request no component changes.
11. Verify ALM through a second component fetch. A write response without persisted read-back is not success.
12. Return control after every operation. The existing `setup_existing_da.py attach` command remains the sole Dev validation, projection, and canonical-completion boundary.
13. If attachment reports a service-owned prerequisite, report it and stop. This path never publishes or removes components.
12. Return control after every operation. The existing `setup_existing_da.py attach` command remains the sole Dev validation, projection, and canonical-completion boundary. For a newly created starter agent, pass the create response's schema name and validate the direct Dev route plus fetched component identity without requiring published Dev configuration.
13. If attachment reports a service-owned prerequisite, report it and stop. Publishing is not attachment remediation for this path; foundation setup never publishes or removes components.
14. Keep response bodies, internal classifications, step IDs, and request details as diagnostic evidence. Translate supported facts into plain maker language; never render raw technical evidence as ordinary maker-facing copy.

## Durable command boundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ workspace completion.
| Claim | Status |
| --- | --- |
| Import uses `POST /copilotstudio/minimalBots/alm/import` | Live-proven in TEST |
| Import uses `api-version=2022-03-01-preview` and `x-ms-client-name: CopilotStudio` | Confirmed by the platform ALM reference |
| The request uses multipart form data with one binary `package` part | Live-proven in TEST |
| Omitting `schemaName` requests create-only behavior | Live-proven through successful create and HTTP 409 collision |
| Supplying a directly validated Dev schema requests replacement | Live-proven in TEST |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ troubleshooting, and any message that asks the maker to perform UI actions.
- Distinguish destinations, UI controls, selected values, and product names.
- Use known context instead of asking the maker to translate generic guidance.

## Current-state language

No historicity means maker-facing text describes only a current observed fact,
a decision the maker must make, an action the maker must take, or a supported
outcome. Authoring rationale and UX meta-intentions remain instruction-only:
response cadence, narration strategy, template selection, render policy, and
commentary about what the conversation will or will not say.

Resolve a UX concern by defining the positive response at its owning boundary:

- successful internal work continues to the next defined maker interaction;
- a required decision uses its defined question and choices;
- a blocked operation states the observed blocker and one supported recovery;
- a completed path uses its defined final handoff.

Terms such as "chatter," "noise," "narration," "render point," "surface,"
"template," "maker-facing," and "UX" describe authoring policy. They are not
setup status, evidence, or instructions for the maker.

## Formatting semantics

Use formatting consistently according to what the text represents:
Expand Down Expand Up @@ -91,6 +110,7 @@ document.
- Formatting every noun for emphasis.
- Internal implementation terms, state paths, tool names, or placeholders in
maker-facing messages.
- Authoring rationale or UX-policy language presented as setup progress.

## Accessibility and resilience

Expand Down
Loading
Loading