From 2d0913568e91eeeceb0133870b54477c563a71bc Mon Sep 17 00:00:00 2001 From: Stefano Demiliani Date: Sun, 23 Aug 2026 22:10:10 +0200 Subject: [PATCH 1/2] feat(community/agents): add AL agent quality guidance - add 20 agent knowledge rules with good and bad AL samples - clarify setup dialog shape, temporary persistence, permissions, profiles, instructions, capability registration, and interface wiring - add the community-owned AL agents review skill - make review fixture discovery layer-aware with custom, community, and Microsoft precedence - document layer-aware evaluation behavior --- ...permissions-intersect-with-assigner.bad.al | 11 ++ ...ermissions-intersect-with-assigner.good.al | 8 ++ ...ent-permissions-intersect-with-assigner.md | 30 +++++ .../agent-profile-narrows-visible-ui.bad.al | 8 ++ .../agent-profile-narrows-visible-ui.good.al | 18 +++ .../agent-profile-narrows-visible-ui.md | 30 +++++ ...-setup-page-is-configuration-dialog.bad.al | 19 +++ ...setup-page-is-configuration-dialog.good.al | 29 +++++ ...gent-setup-page-is-configuration-dialog.md | 26 ++++ ...ent-setup-source-table-is-temporary.bad.al | 29 +++++ ...nt-setup-source-table-is-temporary.good.al | 67 ++++++++++ .../agent-setup-source-table-is-temporary.md | 30 +++++ ...tup-table-keyed-by-user-security-id.bad.al | 24 ++++ ...up-table-keyed-by-user-security-id.good.al | 25 ++++ ...t-setup-table-keyed-by-user-security-id.md | 26 ++++ ...e-error-stops-warning-forces-review.bad.al | 8 ++ ...-error-stops-warning-forces-review.good.al | 26 ++++ ...ssage-error-stops-warning-forces-review.md | 26 ++++ ...t-subscribers-only-in-agent-session.bad.al | 9 ++ ...-subscribers-only-in-agent-session.good.al | 21 +++ ...agent-subscribers-only-in-agent-session.md | 26 ++++ ...pp-agent-calls-need-your-public-api.bad.al | 10 ++ ...p-agent-calls-need-your-public-api.good.al | 21 +++ ...ss-app-agent-calls-need-your-public-api.md | 26 ++++ ...ts-in-install-upgrade-or-background.bad.al | 19 +++ ...s-in-install-upgrade-or-background.good.al | 45 +++++++ ...agents-in-install-upgrade-or-background.md | 26 ++++ ...ult-access-controls-least-privilege.bad.al | 14 ++ ...lt-access-controls-least-privilege.good.al | 16 +++ ...default-access-controls-least-privilege.md | 30 +++++ ...et-default-profile-lives-in-the-app.bad.al | 9 ++ ...t-default-profile-lives-in-the-app.good.al | 20 +++ .../get-default-profile-lives-in-the-app.md | 30 +++++ ...ction-structure-is-role-rules-steps.bad.al | 9 ++ ...tion-structure-is-role-rules-steps.good.al | 17 +++ ...struction-structure-is-role-rules-steps.md | 30 +++++ ...ructions-describe-work-not-tool-ids.bad.al | 7 + ...uctions-describe-work-not-tool-ids.good.al | 12 ++ ...instructions-describe-work-not-tool-ids.md | 30 +++++ ...ly-resource-instructions-on-upgrade.bad.al | 18 +++ ...y-resource-instructions-on-upgrade.good.al | 33 +++++ ...eapply-resource-instructions-on-upgrade.md | 26 ++++ ...er-copilot-capability-for-the-agent.bad.al | 21 +++ ...r-copilot-capability-for-the-agent.good.al | 26 ++++ ...gister-copilot-capability-for-the-agent.md | 30 +++++ .../set-instructions-as-secrettext.bad.al | 19 +++ .../set-instructions-as-secrettext.good.al | 20 +++ .../agents/set-instructions-as-secrettext.md | 26 ++++ ...te-agent-does-not-block-code-create.bad.al | 36 ++++++ ...e-agent-does-not-block-code-create.good.al | 25 ++++ ...create-agent-does-not-block-code-create.md | 26 ++++ ...oming-review-only-for-trusted-input.bad.al | 15 +++ ...ming-review-only-for-trusted-input.good.al | 16 +++ ...-incoming-review-only-for-trusted-input.md | 26 ++++ ...use-documented-instruction-keywords.bad.al | 7 + ...se-documented-instruction-keywords.good.al | 13 ++ .../use-documented-instruction-keywords.md | 30 +++++ .../wire-all-three-agent-interfaces.bad.al | 9 ++ .../wire-all-three-agent-interfaces.good.al | 10 ++ .../agents/wire-all-three-agent-interfaces.md | 30 +++++ community/skills/review/al-agents-review.md | 68 ++++++++++ evaluation/README.md | 6 +- tools/Test-ReviewFixtures.ps1 | 120 +++++++++++++----- 63 files changed, 1489 insertions(+), 34 deletions(-) create mode 100644 community/knowledge/agents/agent-permissions-intersect-with-assigner.bad.al create mode 100644 community/knowledge/agents/agent-permissions-intersect-with-assigner.good.al create mode 100644 community/knowledge/agents/agent-permissions-intersect-with-assigner.md create mode 100644 community/knowledge/agents/agent-profile-narrows-visible-ui.bad.al create mode 100644 community/knowledge/agents/agent-profile-narrows-visible-ui.good.al create mode 100644 community/knowledge/agents/agent-profile-narrows-visible-ui.md create mode 100644 community/knowledge/agents/agent-setup-page-is-configuration-dialog.bad.al create mode 100644 community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al create mode 100644 community/knowledge/agents/agent-setup-page-is-configuration-dialog.md create mode 100644 community/knowledge/agents/agent-setup-source-table-is-temporary.bad.al create mode 100644 community/knowledge/agents/agent-setup-source-table-is-temporary.good.al create mode 100644 community/knowledge/agents/agent-setup-source-table-is-temporary.md create mode 100644 community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.bad.al create mode 100644 community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.good.al create mode 100644 community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.md create mode 100644 community/knowledge/agents/analyze-message-error-stops-warning-forces-review.bad.al create mode 100644 community/knowledge/agents/analyze-message-error-stops-warning-forces-review.good.al create mode 100644 community/knowledge/agents/analyze-message-error-stops-warning-forces-review.md create mode 100644 community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.bad.al create mode 100644 community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.good.al create mode 100644 community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.md create mode 100644 community/knowledge/agents/cross-app-agent-calls-need-your-public-api.bad.al create mode 100644 community/knowledge/agents/cross-app-agent-calls-need-your-public-api.good.al create mode 100644 community/knowledge/agents/cross-app-agent-calls-need-your-public-api.md create mode 100644 community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.bad.al create mode 100644 community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al create mode 100644 community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.md create mode 100644 community/knowledge/agents/get-default-access-controls-least-privilege.bad.al create mode 100644 community/knowledge/agents/get-default-access-controls-least-privilege.good.al create mode 100644 community/knowledge/agents/get-default-access-controls-least-privilege.md create mode 100644 community/knowledge/agents/get-default-profile-lives-in-the-app.bad.al create mode 100644 community/knowledge/agents/get-default-profile-lives-in-the-app.good.al create mode 100644 community/knowledge/agents/get-default-profile-lives-in-the-app.md create mode 100644 community/knowledge/agents/instruction-structure-is-role-rules-steps.bad.al create mode 100644 community/knowledge/agents/instruction-structure-is-role-rules-steps.good.al create mode 100644 community/knowledge/agents/instruction-structure-is-role-rules-steps.md create mode 100644 community/knowledge/agents/instructions-describe-work-not-tool-ids.bad.al create mode 100644 community/knowledge/agents/instructions-describe-work-not-tool-ids.good.al create mode 100644 community/knowledge/agents/instructions-describe-work-not-tool-ids.md create mode 100644 community/knowledge/agents/reapply-resource-instructions-on-upgrade.bad.al create mode 100644 community/knowledge/agents/reapply-resource-instructions-on-upgrade.good.al create mode 100644 community/knowledge/agents/reapply-resource-instructions-on-upgrade.md create mode 100644 community/knowledge/agents/register-copilot-capability-for-the-agent.bad.al create mode 100644 community/knowledge/agents/register-copilot-capability-for-the-agent.good.al create mode 100644 community/knowledge/agents/register-copilot-capability-for-the-agent.md create mode 100644 community/knowledge/agents/set-instructions-as-secrettext.bad.al create mode 100644 community/knowledge/agents/set-instructions-as-secrettext.good.al create mode 100644 community/knowledge/agents/set-instructions-as-secrettext.md create mode 100644 community/knowledge/agents/show-can-create-agent-does-not-block-code-create.bad.al create mode 100644 community/knowledge/agents/show-can-create-agent-does-not-block-code-create.good.al create mode 100644 community/knowledge/agents/show-can-create-agent-does-not-block-code-create.md create mode 100644 community/knowledge/agents/skip-incoming-review-only-for-trusted-input.bad.al create mode 100644 community/knowledge/agents/skip-incoming-review-only-for-trusted-input.good.al create mode 100644 community/knowledge/agents/skip-incoming-review-only-for-trusted-input.md create mode 100644 community/knowledge/agents/use-documented-instruction-keywords.bad.al create mode 100644 community/knowledge/agents/use-documented-instruction-keywords.good.al create mode 100644 community/knowledge/agents/use-documented-instruction-keywords.md create mode 100644 community/knowledge/agents/wire-all-three-agent-interfaces.bad.al create mode 100644 community/knowledge/agents/wire-all-three-agent-interfaces.good.al create mode 100644 community/knowledge/agents/wire-all-three-agent-interfaces.md create mode 100644 community/skills/review/al-agents-review.md diff --git a/community/knowledge/agents/agent-permissions-intersect-with-assigner.bad.al b/community/knowledge/agents/agent-permissions-intersect-with-assigner.bad.al new file mode 100644 index 00000000..eab5ef79 --- /dev/null +++ b/community/knowledge/agents/agent-permissions-intersect-with-assigner.bad.al @@ -0,0 +1,11 @@ +permissionset 50100 "SALES REVIEW AGENT" +{ + Assignable = true; + Permissions = + tabledata "Sales Header" = RIM, + tabledata Customer = R, + tabledata User = RIMD, + tabledata "Access Control" = RIMD, + page "Sales Order" = X, + page "User Card" = X; +} diff --git a/community/knowledge/agents/agent-permissions-intersect-with-assigner.good.al b/community/knowledge/agents/agent-permissions-intersect-with-assigner.good.al new file mode 100644 index 00000000..836c6a59 --- /dev/null +++ b/community/knowledge/agents/agent-permissions-intersect-with-assigner.good.al @@ -0,0 +1,8 @@ +permissionset 50100 "SALES REVIEW AGENT" +{ + Assignable = true; + Permissions = + tabledata "Sales Header" = RIM, + tabledata Customer = R, + page "Sales Order" = X; +} diff --git a/community/knowledge/agents/agent-permissions-intersect-with-assigner.md b/community/knowledge/agents/agent-permissions-intersect-with-assigner.md new file mode 100644 index 00000000..2259f66b --- /dev/null +++ b/community/knowledge/agents/agent-permissions-intersect-with-assigner.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [permissions, assigner, intersection, user-card, least-privilege] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Agent permissions intersect the assigner's; agents cannot configure users + +## Description + +An agent is a user, but it cannot configure users or other agents, and it cannot open sensitive pages such as user cards or permission-set assignment. Effective rights are the intersection of the assigning user's permissions and the agent's permission sets. Granting the agent a wide set does not bypass the assigner's limits, and a wide assigner still cannot give the agent user-admin powers the platform forbids. + +## Best Practice + +Document that intersection. Give the agent only the table and page rights its tasks need. Do not add user-setup or permission-assignment pages to the agent profile or permission sets; those operations will fail by design. + +See sample: `agent-permissions-intersect-with-assigner.good.al`. + +## Anti Pattern + +Permission sets or profiles that include User card, Permission Set Assignment, or agent-admin pages, or comments that the agent runs as SUPER regardless of who assigned it. Detection signal: default access controls or profile including user-administration objects. + +See sample: `agent-permissions-intersect-with-assigner.bad.al`. + +## See also + +`get-default-access-controls-least-privilege.md` covers the permission sets assigned when an agent instance is created. diff --git a/community/knowledge/agents/agent-profile-narrows-visible-ui.bad.al b/community/knowledge/agents/agent-profile-narrows-visible-ui.bad.al new file mode 100644 index 00000000..58b1cba9 --- /dev/null +++ b/community/knowledge/agents/agent-profile-narrows-visible-ui.bad.al @@ -0,0 +1,8 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary) + begin + TempAllProfile."Profile ID" := 'BUSINESS MANAGER'; + TempAllProfile.Insert(); + end; +} diff --git a/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al b/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al new file mode 100644 index 00000000..9a3e52a3 --- /dev/null +++ b/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al @@ -0,0 +1,18 @@ +profile "SALES REVIEW AGENT" +{ + Caption = 'Sales Review Agent'; + Description = 'Restricted UI for the Sales Review Agent.'; + RoleCenter = "Order Processor Role Center"; + Customizations = "Sales Review Agent Sales Ord."; +} + +pagecustomization "Sales Review Agent Sales Ord." customizes "Sales Order" +{ + actions + { + modify(Post) + { + Visible = false; + } + } +} diff --git a/community/knowledge/agents/agent-profile-narrows-visible-ui.md b/community/knowledge/agents/agent-profile-narrows-visible-ui.md new file mode 100644 index 00000000..30d286a3 --- /dev/null +++ b/community/knowledge/agents/agent-profile-narrows-visible-ui.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [profile, page-customization, hidden-actions, tooltip, role-center] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Give the agent a dedicated profile that hides unrelated UI + +## Description + +The agent only sees what its profile shows. Extra actions, views, and Role Center tiles become extra tools and extra tokens. Accuracy and cost both get worse as the UI widens. A human Order Processor profile is usually far too broad. Tooltips on the remaining actions are part of the tool description. + +## Best Practice + +Ship an agent-specific profile and page customizations: hide unrelated actions, keep descriptive tooltips, add Role Center links to the few pages the agent should open. Prefer fewer navigation hops. + +See sample: `agent-profile-narrows-visible-ui.good.al`. + +## Anti Pattern + +Assigning `BUSINESS MANAGER` or `ORDER PROCESSOR` as `GetDefaultProfile` so the agent can do anything. Detection signal: default profile equal to a full-user role with no agent page customizations. + +See sample: `agent-profile-narrows-visible-ui.bad.al`. + +## See also + +`get-default-profile-lives-in-the-app.md` covers packaging and assigning the profile that this rule narrows. diff --git a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.bad.al b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.bad.al new file mode 100644 index 00000000..c23bec10 --- /dev/null +++ b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.bad.al @@ -0,0 +1,19 @@ +page 50100 "Sales Review Agent Setup" +{ + PageType = Card; + Caption = 'Set up Sales Review Agent'; + SourceTable = "Sales Review Agent Setup"; + + layout + { + area(Content) + { + field(ReviewThreshold; Rec."Review Threshold") + { + ApplicationArea = All; + Caption = 'Review Threshold'; + ToolTip = 'Specifies the threshold used when the agent requests a review.'; + } + } + } +} diff --git a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al new file mode 100644 index 00000000..fb76031c --- /dev/null +++ b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al @@ -0,0 +1,29 @@ +page 50100 "Sales Review Agent Setup" +{ + PageType = ConfigurationDialog; + Caption = 'Set up Sales Review Agent'; + SourceTable = "Sales Review Agent Setup"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + part(AgentSetupPart; "Agent Setup Part") + { + ApplicationArea = All; + UpdatePropagation = Both; + } + group(AdditionalConfiguration) + { + Caption = 'Additional Configuration'; + field(ReviewThreshold; Rec."Review Threshold") + { + ApplicationArea = All; + Caption = 'Review Threshold'; + ToolTip = 'Specifies the threshold used when the agent requests a review.'; + } + } + } + } +} diff --git a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md new file mode 100644 index 00000000..f3460103 --- /dev/null +++ b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [configurationdialog, agent-setup-part, setup-page, pagetype, system-actions] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Agent setup pages use ConfigurationDialog and the Agent Setup Part + +## Description + +Instance setup is not a Card or StandardDialog. The toolkit expects `PageType = ConfigurationDialog` so OK and Cancel are system actions, plus the built-in `Agent Setup Part` for name, display name, state, and access. A Card with custom fields only drops those shared controls and the AI-use notices the part carries. + +## Best Practice + +Declare `PageType = ConfigurationDialog`, host `part(...; "Agent Setup Part")`, and put agent-specific fields in another group. Keep system OK/Cancel. Use a temporary source record and defer persistence until Update, as described in `agent-setup-source-table-is-temporary.md`. + +See sample: `agent-setup-page-is-configuration-dialog.good.al`. + +## Anti Pattern + +A Card or StandardDialog setup page with no `Agent Setup Part`. Detection signal: setup page ID from `IAgentFactory` / `IAgentMetadata` whose page is not `ConfigurationDialog` or has no `Agent Setup Part`. + +See sample: `agent-setup-page-is-configuration-dialog.bad.al`. diff --git a/community/knowledge/agents/agent-setup-source-table-is-temporary.bad.al b/community/knowledge/agents/agent-setup-source-table-is-temporary.bad.al new file mode 100644 index 00000000..96ef570d --- /dev/null +++ b/community/knowledge/agents/agent-setup-source-table-is-temporary.bad.al @@ -0,0 +1,29 @@ +page 50100 "Sales Review Agent Setup" +{ + PageType = ConfigurationDialog; + SourceTable = "Sales Review Agent Setup"; + + layout + { + area(Content) + { + field(ReviewThreshold; Rec."Review Threshold") + { + ApplicationArea = All; + Caption = 'Review Threshold'; + ToolTip = 'Specifies the threshold used when the agent requests a review.'; + + trigger OnValidate() + begin + Rec.Modify(true); + end; + } + } + } + + trigger OnOpenPage() + begin + if Rec.IsEmpty() then + Rec.Insert(true); + end; +} diff --git a/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al b/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al new file mode 100644 index 00000000..7326c925 --- /dev/null +++ b/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al @@ -0,0 +1,67 @@ +page 50100 "Sales Review Agent Setup" +{ + PageType = ConfigurationDialog; + SourceTable = "Sales Review Agent Setup"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + part(AgentSetupPart; "Agent Setup Part") + { + ApplicationArea = All; + UpdatePropagation = Both; + } + group(AdditionalConfiguration) + { + Caption = 'Additional Configuration'; + field(ReviewThreshold; Rec."Review Threshold") + { + ApplicationArea = All; + Caption = 'Review Threshold'; + ToolTip = 'Specifies the threshold used when the agent requests a review.'; + } + } + } + } + + trigger OnOpenPage() + var + SalesReviewAgentSetup: Record "Sales Review Agent Setup"; + begin + if IsNullGuid(Rec."User Security ID") then + exit; + if SalesReviewAgentSetup.Get(Rec."User Security ID") then + Rec := SalesReviewAgentSetup; + end; + + trigger OnQueryClosePage(CloseAction: Action): Boolean + var + AgentSetup: Codeunit "Agent Setup"; + AgentSetupBuffer: Record "Agent Setup Buffer"; + begin + if CloseAction = CloseAction::Cancel then + exit(true); + CurrPage.AgentSetupPart.Page.GetAgentSetupBuffer(AgentSetupBuffer); + if AgentSetup.GetChangesMade(AgentSetupBuffer) then + Rec."User Security ID" := AgentSetup.SaveChanges(AgentSetupBuffer); + if IsNullGuid(Rec."User Security ID") then + exit(true); + SaveCustomProperties(); + exit(true); + end; + + local procedure SaveCustomProperties() + var + SalesReviewAgentSetup: Record "Sales Review Agent Setup"; + begin + if not SalesReviewAgentSetup.Get(Rec."User Security ID") then begin + SalesReviewAgentSetup.Init(); + SalesReviewAgentSetup."User Security ID" := Rec."User Security ID"; + SalesReviewAgentSetup.Insert(true); + end; + SalesReviewAgentSetup."Review Threshold" := Rec."Review Threshold"; + SalesReviewAgentSetup.Modify(true); + end; +} diff --git a/community/knowledge/agents/agent-setup-source-table-is-temporary.md b/community/knowledge/agents/agent-setup-source-table-is-temporary.md new file mode 100644 index 00000000..8f31aa05 --- /dev/null +++ b/community/knowledge/agents/agent-setup-source-table-is-temporary.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [sourcetabletemporary, configurationdialog, savechanges, cancel, draft] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Keep the agent setup page source temporary until Update + +## Description + +ConfigurationDialog setup is a draft: the user can Cancel without writing. That only works if `SourceTableTemporary = true` and custom fields stay in memory until Update. Writing the real table in OnValidate or OnOpenPage commits a partial agent when the dialog errors or is cancelled. + +## Best Practice + +Mark the page `SourceTableTemporary = true`. Copy into the temp record on open. Persist the Agent Setup buffer and custom fields only from the close path when the action is not Cancel, using `Agent Setup.GetChangesMade` / `SaveChanges`. + +See sample: `agent-setup-source-table-is-temporary.good.al`. + +## Anti Pattern + +A non-temporary source table, or `Insert`/`Modify` on the persisted setup row from field OnValidate. Detection signal: agent `ConfigurationDialog` without `SourceTableTemporary = true`, or database writes before Update. + +See sample: `agent-setup-source-table-is-temporary.bad.al`. + +## See also + +`agent-setup-page-is-configuration-dialog.md` defines the setup page shape that uses this draft lifecycle. diff --git a/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.bad.al b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.bad.al new file mode 100644 index 00000000..cff27381 --- /dev/null +++ b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.bad.al @@ -0,0 +1,24 @@ +table 50100 "Sales Review Agent Setup" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "Primary Key"; Code[10]) + { + Caption = 'Primary Key'; + } + field(10; "Review Threshold"; Decimal) + { + Caption = 'Review Threshold'; + } + } + + keys + { + key(PK; "Primary Key") + { + Clustered = true; + } + } +} diff --git a/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.good.al b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.good.al new file mode 100644 index 00000000..f5b1f9b7 --- /dev/null +++ b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.good.al @@ -0,0 +1,25 @@ +table 50100 "Sales Review Agent Setup" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "User Security ID"; Guid) + { + Caption = 'User Security ID'; + DataClassification = EndUserPseudonymousIdentifiers; + } + field(10; "Review Threshold"; Decimal) + { + Caption = 'Review Threshold'; + } + } + + keys + { + key(PK; "User Security ID") + { + Clustered = true; + } + } +} diff --git a/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.md b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.md new file mode 100644 index 00000000..c59a9f16 --- /dev/null +++ b/community/knowledge/agents/agent-setup-table-keyed-by-user-security-id.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [user-security-id, setup-table, primary-key, agent-instance, guid] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Agent setup tables are keyed by User Security ID + +## Description + +Each agent instance is a user. Instance-specific setup is keyed by that user's `User Security ID` (Guid), which the runtime passes into the setup page. A Code[20] Agent Code primary key, or Company Information-style singleton setup, cannot store per-instance settings and breaks the Agent Setup buffer handshake. + +## Best Practice + +Give the setup table a Guid field `User Security ID` as the clustered primary key. Other settings are attributes of that key. When the page opens, `Get` or insert by the Guid the Agent Setup part already holds. + +See sample: `agent-setup-table-keyed-by-user-security-id.good.al`. + +## Anti Pattern + +A setup table keyed by Code, Integer, or with no Guid user key, then mapping one row to every instance. Detection signal: source table of the agent setup page whose primary key is not `User Security ID`. + +See sample: `agent-setup-table-keyed-by-user-security-id.bad.al`. diff --git a/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.bad.al b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.bad.al new file mode 100644 index 00000000..d75db08b --- /dev/null +++ b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.bad.al @@ -0,0 +1,8 @@ +codeunit 50100 "Sales Review Agent Task" +{ + procedure AnalyzeAgentTaskMessage(AgentTaskMessage: Record "Agent Task Message"; var Annotations: Record "Agent Annotation") + begin + // No validation. Combined with SetRequiresReview(false) this auto-runs + // untrusted input. Warnings are the only way to force a review later. + end; +} diff --git a/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.good.al b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.good.al new file mode 100644 index 00000000..27d7837f --- /dev/null +++ b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.good.al @@ -0,0 +1,26 @@ +codeunit 50100 "Sales Review Agent Task" +{ + procedure AnalyzeAgentTaskMessage(AgentTaskMessage: Record "Agent Task Message"; var Annotations: Record "Agent Annotation") + var + AgentMessage: Codeunit "Agent Message"; + NotRelevantMsg: Label 'Message is not a sales order task.'; + NotRelevantDetailsTxt: Label 'Provide a message related to sales order review.'; + begin + if AgentTaskMessage.Type = AgentTaskMessage.Type::Output then begin + AgentMessage.UpdateText(AgentTaskMessage, AgentMessage.GetText(AgentTaskMessage) + '\n\nWritten with the help of AI'); + exit; + end; + if not IsRelevant(AgentMessage.GetText(AgentTaskMessage)) then begin + Annotations.Code := 'RELEVANCE001'; + Annotations.Severity := Annotations.Severity::Warning; + Annotations.Message := NotRelevantMsg; + Annotations.Details := NotRelevantDetailsTxt; + Annotations.Insert(); + end; + end; + + local procedure IsRelevant(MessageText: Text): Boolean + begin + exit(MessageText <> ''); + end; +} diff --git a/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.md b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.md new file mode 100644 index 00000000..f8c6eb5d --- /dev/null +++ b/community/knowledge/agents/analyze-message-error-stops-warning-forces-review.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [analyzeagenttaskmessage, agent-annotation, error, warning, setrequiresreview] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AnalyzeAgentTaskMessage: Error stops the task; Warning still requires review + +## Description + +`IAgentTaskExecution.AnalyzeAgentTaskMessage` runs on inbound and outbound messages. An Error annotation stops processing. A Warning annotation requests user intervention. If analysis returns Warning, the platform still requires approval even when the incoming message used `SetRequiresReview(false)`. Output text can be rewritten here (signature, redaction). + +## Best Practice + +Validate inbound payloads in analysis: Error when the task must not run; Warning when a human must confirm. For outbound messages, adjust text in this method rather than in a later subscriber. Do not rely on skip-review to bypass warnings. + +See sample: `analyze-message-error-stops-warning-forces-review.good.al`. + +## Anti Pattern + +Ignoring analysis entirely, or emitting Warning while documenting that `SetRequiresReview(false)` means unattended run. Detection signal: empty `AnalyzeAgentTaskMessage` plus skip-review on external input. + +See sample: `analyze-message-error-stops-warning-forces-review.bad.al`. diff --git a/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.bad.al b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.bad.al new file mode 100644 index 00000000..998066c7 --- /dev/null +++ b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.bad.al @@ -0,0 +1,9 @@ +codeunit 50101 "Sales Review Agent Events" +{ + [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterInsertEvent, '', false, false)] + local procedure OnAfterInsertSalesHeader(var Rec: Record "Sales Header") + begin + // Runs for every user session, not only the agent. + Message('Keep going, agent.'); + end; +} diff --git a/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.good.al b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.good.al new file mode 100644 index 00000000..9246d336 --- /dev/null +++ b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.good.al @@ -0,0 +1,21 @@ +codeunit 50102 "Agent Session Events" +{ + Access = Internal; + SingleInstance = true; + InherentEntitlements = X; + InherentPermissions = X; + + var + GlobalAgentEvents: Codeunit "Sales Review Agent Events"; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Initialization", OnAfterInitialization, '', false, false)] + local procedure RegisterSubscribersOnAfterInitialization() + var + AgentSession: Codeunit "Agent Session"; + AgentMetadataProvider: Enum "Agent Metadata Provider"; + begin + if not AgentSession.IsAgentSession(AgentMetadataProvider) then + exit; + if BindSubscription(GlobalAgentEvents) then; + end; +} diff --git a/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.md b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.md new file mode 100644 index 00000000..3d18818e --- /dev/null +++ b/community/knowledge/agents/bind-agent-subscribers-only-in-agent-session.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [agent-session, isagentsession, bindsubscription, system-initialization, singleinstance] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Bind extra agent subscribers only inside an agent session + +## Description + +Page-filter tweaks, extra validation, and prompt dialogs for the agent should not run for every user. `Agent Session.IsAgentSession` distinguishes agent UI sessions. Binding those subscribers on `System Initialization` only when the session is an agent session avoids global subscriber cost. Models register `SingleInstance` table subscribers unconditionally. + +## Best Practice + +On `OnAfterInitialization`, exit unless `Agent Session.IsAgentSession`. Then `BindSubscription` a single-instance codeunit that holds the current task id. Keep those subscribers internal. + +See sample: `bind-agent-subscribers-only-in-agent-session.good.al`. + +## Anti Pattern + +Event subscribers on `Sales Header` OnAfterInsert that always `Message` the agent, with no `IsAgentSession` guard. Detection signal: agent-only behaviour in a static subscriber that is not bind-gated. + +See sample: `bind-agent-subscribers-only-in-agent-session.bad.al`. diff --git a/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.bad.al b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.bad.al new file mode 100644 index 00000000..93a7afe6 --- /dev/null +++ b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.bad.al @@ -0,0 +1,10 @@ +codeunit 50110 "Other App Agent Hook" +{ + procedure RenameForeignAgent(AgentUserSecurityId: Guid) + var + Agent: Codeunit Agent; + begin + // Fails at runtime when the instance was defined in another app. + Agent.SetDisplayName(AgentUserSecurityId, 'Updated Name'); + end; +} diff --git a/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.good.al b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.good.al new file mode 100644 index 00000000..51181e81 --- /dev/null +++ b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.good.al @@ -0,0 +1,21 @@ +codeunit 50110 "Sales Review Agent API" +{ + Access = Public; + + procedure SetDisplayName(AgentUserSecurityId: Guid; NewDisplayName: Text[80]) + var + Agent: Codeunit Agent; + begin + Agent.SetDisplayName(AgentUserSecurityId, NewDisplayName); + end; + + procedure SetActiveState(AgentUserSecurityId: Guid; ActivateAgent: Boolean) + var + Agent: Codeunit Agent; + begin + if ActivateAgent then + Agent.Activate(AgentUserSecurityId) + else + Agent.Deactivate(AgentUserSecurityId); + end; +} diff --git a/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.md b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.md new file mode 100644 index 00000000..41c1c2fd --- /dev/null +++ b/community/knowledge/agents/cross-app-agent-calls-need-your-public-api.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [cross-app, public-api, agent-create, isolation, access-public] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Other apps cannot call the toolkit APIs on your agent; publish your own API + +## Description + +For isolation, `Agent`, `Agent Task Builder`, and related toolkit codeunits error when the target instance belongs to another app. There is no supported way to pass another extension's metadata provider into `SetInstructions` or `Create`. Partners who need to enqueue work must call a public API you own. + +## Best Practice + +Expose a public codeunit in the agent app (`Access = Public`) whose procedures take `User Security ID` and forward to `Agent` / `Agent Task Builder`. Document that surface as the integration contract. Keep toolkit calls inside that app. + +See sample: `cross-app-agent-calls-need-your-public-api.good.al`. + +## Anti Pattern + +From app B, calling `Agent.SetDisplayName` or `Agent.Create` with app A's metadata provider. Detection signal: toolkit agent APIs used with an `Agent Metadata Provider` value not declared in the same app. + +See sample: `cross-app-agent-calls-need-your-public-api.bad.al`. diff --git a/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.bad.al b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.bad.al new file mode 100644 index 00000000..26cbb384 --- /dev/null +++ b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.bad.al @@ -0,0 +1,19 @@ +codeunit 50100 "Sales Review Agent Install" +{ + Subtype = Install; + + trigger OnInstallAppPerCompany() + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + AgentUserSecurityId: Guid; + begin + // Create requires an interactive session. Install is not one. + AgentUserSecurityId := Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + Agent.Activate(AgentUserSecurityId); + end; +} diff --git a/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al new file mode 100644 index 00000000..198f0afc --- /dev/null +++ b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al @@ -0,0 +1,45 @@ +page 50100 "Sales Review Agent Setup" +{ + PageType = ConfigurationDialog; + ApplicationArea = All; + SourceTable = "Sales Review Agent Setup"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + part(AgentSetupPart; "Agent Setup Part") + { + ApplicationArea = All; + UpdatePropagation = Both; + } + } + } + + trigger OnQueryClosePage(CloseAction: Action): Boolean + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + AgentUserSecurityId: Guid; + begin + if CloseAction = CloseAction::Cancel then + exit(true); + + AgentUserSecurityId := Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + Agent.SetInstructions(AgentUserSecurityId, GetInstructions()); + Agent.Activate(AgentUserSecurityId); + exit(true); + end; + + local procedure GetInstructions() Instructions: SecretText + var + InstructionsNameTxt: Label 'Instructions.txt', Locked = true; + begin + Instructions := NavApp.GetResourceAsText(InstructionsNameTxt); + end; +} diff --git a/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.md b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.md new file mode 100644 index 00000000..41d0573f --- /dev/null +++ b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [agent-create, install, upgrade, job-queue, interactive-session, background] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Do not create agent instances from install, upgrade, or background sessions + +## Description + +`Agent.Create` requires an interactive user session. The platform blocks creation from install codeunits, upgrade codeunits, and background sessions (job queue, scheduled tasks). Packaging an agent in an app does not mean spinning up instances at install. Models still call `Create` from `OnInstallAppPerCompany` to activate the agent. + +## Best Practice + +Create instances from a setup page, a wizard, or another UI-driven path after the user is in a client session. Apply instructions and `Activate` there. For existing companies after an upgrade, document that an admin must open setup; do not create from the upgrade codeunit. + +See sample: `do-not-create-agents-in-install-upgrade-or-background.good.al`. + +## Anti Pattern + +`Agent.Create` inside `OnInstallAppPerCompany`, `OnUpgradePerCompany`, or a job-queue codeunit. The call fails at runtime even if it compiles. Detection signal: `Agent.Create` in `Subtype = Install`, `Subtype = Upgrade`, or a non-UI session. + +See sample: `do-not-create-agents-in-install-upgrade-or-background.bad.al`. diff --git a/community/knowledge/agents/get-default-access-controls-least-privilege.bad.al b/community/knowledge/agents/get-default-access-controls-least-privilege.bad.al new file mode 100644 index 00000000..618f964f --- /dev/null +++ b/community/knowledge/agents/get-default-access-controls-least-privilege.bad.al @@ -0,0 +1,14 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure GetDefaultAccessControls(var TempAccessControlBuffer: Record "Access Control Buffer" temporary) + var + BaseApplicationAppIdTok: Label '437dbf0e-84ff-417a-965d-ed2bb9650972', Locked = true; + begin + Clear(TempAccessControlBuffer); + TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name")); + TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::System; + TempAccessControlBuffer."App ID" := BaseApplicationAppIdTok; + TempAccessControlBuffer."Role ID" := 'D365 BUS FULL ACCESS'; + TempAccessControlBuffer.Insert(); + end; +} diff --git a/community/knowledge/agents/get-default-access-controls-least-privilege.good.al b/community/knowledge/agents/get-default-access-controls-least-privilege.good.al new file mode 100644 index 00000000..e3e6716e --- /dev/null +++ b/community/knowledge/agents/get-default-access-controls-least-privilege.good.al @@ -0,0 +1,16 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure GetDefaultAccessControls(var TempAccessControlBuffer: Record "Access Control Buffer" temporary) + var + CurrentModuleInfo: ModuleInfo; + RoleIdTok: Label 'SALES REVIEW AGENT', Locked = true; + begin + NavApp.GetCurrentModuleInfo(CurrentModuleInfo); + Clear(TempAccessControlBuffer); + TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name")); + TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::Tenant; + TempAccessControlBuffer."App ID" := CurrentModuleInfo.Id; + TempAccessControlBuffer."Role ID" := RoleIdTok; + TempAccessControlBuffer.Insert(); + end; +} diff --git a/community/knowledge/agents/get-default-access-controls-least-privilege.md b/community/knowledge/agents/get-default-access-controls-least-privilege.md new file mode 100644 index 00000000..c0e87b41 --- /dev/null +++ b/community/knowledge/agents/get-default-access-controls-least-privilege.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [getdefaultaccesscontrols, access-control-buffer, permissionset, least-privilege, iagentfactory] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Default agent permission sets must exist in AL and stay least privilege + +## Description + +`IAgentFactory.GetDefaultAccessControls` fills a temporary `Access Control Buffer` used when an instance is created. Permission sets that exist only as user-created sets in a sandbox are missing in the next environment. Granting `D365 BUS FULL ACCESS` or SUPER gives the agent a user-sized blast radius. Effective rights are still the intersection with the assigning user's permissions. + +## Best Practice + +Insert only the permission sets the agent needs, with the correct Scope and App ID. Recreate any BC-only sets as AL permissionset objects first. Prefer a dedicated permission set over a full-user role. + +See sample: `get-default-access-controls-least-privilege.good.al`. + +## Anti Pattern + +Empty `GetDefaultAccessControls`, or inserting `SUPER` / `D365 BUS FULL ACCESS` because it made the demo work. Detection signal: Role ID on the default buffer that is a full-user role, or a set that is not in the app. + +See sample: `get-default-access-controls-least-privilege.bad.al`. + +## See also + +`agent-permissions-intersect-with-assigner.md` explains the platform limits that still apply after default access controls are assigned. diff --git a/community/knowledge/agents/get-default-profile-lives-in-the-app.bad.al b/community/knowledge/agents/get-default-profile-lives-in-the-app.bad.al new file mode 100644 index 00000000..ba713e44 --- /dev/null +++ b/community/knowledge/agents/get-default-profile-lives-in-the-app.bad.al @@ -0,0 +1,9 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary) + begin + // Profile exists only as a user personalization in the design sandbox. + TempAllProfile."Profile ID" := 'SALES REVIEW SANDBOX'; + TempAllProfile.Insert(); + end; +} diff --git a/community/knowledge/agents/get-default-profile-lives-in-the-app.good.al b/community/knowledge/agents/get-default-profile-lives-in-the-app.good.al new file mode 100644 index 00000000..455fba69 --- /dev/null +++ b/community/knowledge/agents/get-default-profile-lives-in-the-app.good.al @@ -0,0 +1,20 @@ +profile "SALES REVIEW AGENT" +{ + Caption = 'Sales Review Agent'; + Description = 'UI surface for the Sales Review Agent.'; + RoleCenter = "Order Processor Role Center"; + Customizations = "Sales Review Agent Sales Ord."; +} + +codeunit 50100 "Sales Review Agent Factory" +{ + procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary) + var + Agent: Codeunit Agent; + CurrentModuleInfo: ModuleInfo; + DefaultProfileTok: Label 'SALES REVIEW AGENT', Locked = true; + begin + NavApp.GetCurrentModuleInfo(CurrentModuleInfo); + Agent.PopulateDefaultProfile(DefaultProfileTok, CurrentModuleInfo.Id, TempAllProfile); + end; +} diff --git a/community/knowledge/agents/get-default-profile-lives-in-the-app.md b/community/knowledge/agents/get-default-profile-lives-in-the-app.md new file mode 100644 index 00000000..25103b54 --- /dev/null +++ b/community/knowledge/agents/get-default-profile-lives-in-the-app.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [getdefaultprofile, profile, page-customization, populatedefaultprofile, role-center] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# The default agent profile must be an AL profile in the app + +## Description + +`IAgentFactory.GetDefaultProfile` assigns the Role Center and page customizations the agent UI-navigates. A profile built only in the client, or page personalization that was never exported, is absent after deploy. `Agent.PopulateDefaultProfile` still needs a profile ID that exists in the current module. + +## Best Practice + +Ship a `profile` object (and page customizations) in the app. In `GetDefaultProfile`, call `Agent.PopulateDefaultProfile` with that profile ID and `NavApp.GetCurrentModuleInfo`. Include UI-exported customizations as AL. + +See sample: `get-default-profile-lives-in-the-app.good.al`. + +## Anti Pattern + +Setting `TempAllProfile."Profile ID"` to a client-only profile, or skipping `GetDefaultProfile`. Detection signal: factory default profile ID with no matching `profile` object in the app. + +See sample: `get-default-profile-lives-in-the-app.bad.al`. + +## See also + +`agent-profile-narrows-visible-ui.md` explains which UI the app-owned profile should expose. diff --git a/community/knowledge/agents/instruction-structure-is-role-rules-steps.bad.al b/community/knowledge/agents/instruction-structure-is-role-rules-steps.bad.al new file mode 100644 index 00000000..27d1a089 --- /dev/null +++ b/community/knowledge/agents/instruction-structure-is-role-rules-steps.bad.al @@ -0,0 +1,9 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + var + PromptLbl: Label 'Check customer credit for the given sales order. Document the result.', Locked = true; + begin + Instructions := PromptLbl; + end; +} diff --git a/community/knowledge/agents/instruction-structure-is-role-rules-steps.good.al b/community/knowledge/agents/instruction-structure-is-role-rules-steps.good.al new file mode 100644 index 00000000..5876151a --- /dev/null +++ b/community/knowledge/agents/instruction-structure-is-role-rules-steps.good.al @@ -0,0 +1,17 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + var + Builder: TextBuilder; + begin + Builder.AppendLine('# Responsibilities'); + Builder.AppendLine('You validate sales orders against customer credit and hold status.'); + Builder.AppendLine('# Guidelines'); + Builder.AppendLine('Always request a review before posting or sending external mail.'); + Builder.AppendLine('# Instructions'); + Builder.AppendLine('1. Open the sales order named in the task.'); + Builder.AppendLine('2. Check credit limit and overdue balance.'); + Builder.AppendLine('3. Document the result on the order and request a review.'); + Instructions := Builder.ToText(); + end; +} diff --git a/community/knowledge/agents/instruction-structure-is-role-rules-steps.md b/community/knowledge/agents/instruction-structure-is-role-rules-steps.md new file mode 100644 index 00000000..1b56625d --- /dev/null +++ b/community/knowledge/agents/instruction-structure-is-role-rules-steps.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [instructions, responsibilities, guidelines, steps, setinstructions] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Instruction documents use responsibilities, guidelines, then ordered steps + +## Description + +The runtime treats instructions as the agent's standing prompt. A one-line goal produces inconsistent navigation. Microsoft's instruction framework is three layers: responsibilities (what the agent owns), guidelines (rules for every task), and instructions (ordered steps per task, with substeps). That structure is BC-specific, not generic prompt flavour. + +## Best Practice + +Store a document that states responsibilities, then non-negotiable guidelines (when to request a review, when not to post), then numbered steps for each task. Keep that text in the resource you pass to `SetInstructions`. + +See sample: `instruction-structure-is-role-rules-steps.good.al`. + +## Anti Pattern + +A single sentence such as Check customer credit for the sales order. Detection signal: instruction resource or `SetInstructions` payload with no responsibilities / guidelines / steps sections. + +See sample: `instruction-structure-is-role-rules-steps.bad.al`. + +## See also + +`instructions-describe-work-not-tool-ids.md` and `use-documented-instruction-keywords.md` define how to write the steps inside this structure. diff --git a/community/knowledge/agents/instructions-describe-work-not-tool-ids.bad.al b/community/knowledge/agents/instructions-describe-work-not-tool-ids.bad.al new file mode 100644 index 00000000..46625475 --- /dev/null +++ b/community/knowledge/agents/instructions-describe-work-not-tool-ids.bad.al @@ -0,0 +1,7 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + begin + Instructions := 'Open page 42. Invoke action Post_Promoted. Use tool SalesOrder.CreditCheck_v3.'; + end; +} diff --git a/community/knowledge/agents/instructions-describe-work-not-tool-ids.good.al b/community/knowledge/agents/instructions-describe-work-not-tool-ids.good.al new file mode 100644 index 00000000..75fd9869 --- /dev/null +++ b/community/knowledge/agents/instructions-describe-work-not-tool-ids.good.al @@ -0,0 +1,12 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + var + Builder: TextBuilder; + begin + Builder.AppendLine('Memorize the sales order number from the task.'); + Builder.AppendLine('Set the order on hold when credit fails, with a reason.'); + Builder.AppendLine('When credit passes, request a review before posting the order.'); + Instructions := Builder.ToText(); + end; +} diff --git a/community/knowledge/agents/instructions-describe-work-not-tool-ids.md b/community/knowledge/agents/instructions-describe-work-not-tool-ids.md new file mode 100644 index 00000000..a1a536ad --- /dev/null +++ b/community/knowledge/agents/instructions-describe-work-not-tool-ids.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [instructions, tools, invoke-action, memorize, page-actions] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Instructions describe outcomes, not page action or tool names + +## Description + +Agent tools are the UI the profile exposes. Action names and tool ids change across pages and versions. Best-practice guidance is to say what to accomplish, not which tool to invoke. Page state is also not fully in history; values needed later must be memorized. Models paste Promoted action names into the prompt. + +## Best Practice + +Write steps as business outcomes (release the order, set the hold reason). Tell the agent to memorize identifiers it must reuse. Do not hard-code action captions or tool ids. + +See sample: `instructions-describe-work-not-tool-ids.good.al`. + +## Anti Pattern + +Instructions that say invoke SalesOrder.Post_Promoted or use tool page-42-action-3. Detection signal: instruction text containing Promoted action names or tool identifiers. + +See sample: `instructions-describe-work-not-tool-ids.bad.al`. + +## See also + +`instruction-structure-is-role-rules-steps.md` defines the containing document structure, and `use-documented-instruction-keywords.md` identifies runtime-recognized phrases. diff --git a/community/knowledge/agents/reapply-resource-instructions-on-upgrade.bad.al b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.bad.al new file mode 100644 index 00000000..56ca2287 --- /dev/null +++ b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.bad.al @@ -0,0 +1,18 @@ +codeunit 50100 "Sales Review Agent Create" +{ + procedure CreateWithInstructions() + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + AgentUserSecurityId: Guid; + InstructionsNameTxt: Label 'Instructions.txt', Locked = true; + begin + AgentUserSecurityId := Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + // Only new instances get the resource. Upgrades never re-apply it. + Agent.SetInstructions(AgentUserSecurityId, NavApp.GetResourceAsText(InstructionsNameTxt)); + end; +} diff --git a/community/knowledge/agents/reapply-resource-instructions-on-upgrade.good.al b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.good.al new file mode 100644 index 00000000..da255d01 --- /dev/null +++ b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.good.al @@ -0,0 +1,33 @@ +codeunit 50100 "Sales Review Agent Upgrade" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + var + Agent: Codeunit Agent; + UpgradeTag: Codeunit "Upgrade Tag"; + Instructions: SecretText; + AgentUserSecurityIds: List of [Guid]; + AgentUserSecurityId: Guid; + TagTxt: Label 'SALESREVIEW-INSTR-2.0.0', Locked = true; + InstructionsNameTxt: Label 'Instructions.txt', Locked = true; + begin + if UpgradeTag.HasUpgradeTag(TagTxt) then + exit; + Instructions := NavApp.GetResourceAsText(InstructionsNameTxt); + AgentUserSecurityIds := GetExistingAgentUserIds(); + foreach AgentUserSecurityId in AgentUserSecurityIds do + Agent.SetInstructions(AgentUserSecurityId, Instructions); + UpgradeTag.SetUpgradeTag(TagTxt); + end; + + local procedure GetExistingAgentUserIds() AgentUserSecurityIds: List of [Guid] + var + SalesReviewAgentSetup: Record "Sales Review Agent Setup"; + begin + if SalesReviewAgentSetup.FindSet() then + repeat + AgentUserSecurityIds.Add(SalesReviewAgentSetup."User Security ID"); + until SalesReviewAgentSetup.Next() = 0; + end; +} diff --git a/community/knowledge/agents/reapply-resource-instructions-on-upgrade.md b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.md new file mode 100644 index 00000000..5345d254 --- /dev/null +++ b/community/knowledge/agents/reapply-resource-instructions-on-upgrade.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [upgrade, setinstructions, navapp-getresourceastext, existing-instances, upgrade-tag] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Reapply resource instructions to existing agent instances on upgrade + +## Description + +Static instructions stored as an app resource are copied onto an instance only when you call `SetInstructions`. Shipping a new `Instructions.txt` in version 2.0 does not update agents created under 1.0. Models change the resource and assume running instances pick it up. + +## Best Practice + +In the upgrade codeunit, find existing instances of your metadata provider and call `SetInstructions` again with `NavApp.GetResourceAsText`. Guard with an upgrade tag so the rewrite runs once per version that changes the file. + +See sample: `reapply-resource-instructions-on-upgrade.good.al`. + +## Anti Pattern + +Editing only the resource file, or calling `SetInstructions` solely from the first-time setup path. Detection signal: instruction resource in `resourceFolders` with no upgrade procedure that re-applies it. + +See sample: `reapply-resource-instructions-on-upgrade.bad.al`. diff --git a/community/knowledge/agents/register-copilot-capability-for-the-agent.bad.al b/community/knowledge/agents/register-copilot-capability-for-the-agent.bad.al new file mode 100644 index 00000000..eaf64402 --- /dev/null +++ b/community/knowledge/agents/register-copilot-capability-for-the-agent.bad.al @@ -0,0 +1,21 @@ +enumextension 50100 "Sales Review Agent Metadata" extends "Agent Metadata Provider" +{ + value(50100; "Sales Review Agent") + { + Caption = 'Sales Review Agent'; + Implementation = IAgentFactory = "Sales Review Agent Factory", + IAgentMetadata = "Sales Review Agent Metadata", + IAgentTaskExecution = "Sales Review Agent Task"; + } +} + +codeunit 50101 "Sales Review Agent Install" +{ + Subtype = Install; + Access = Internal; + + trigger OnInstallAppPerDatabase() + begin + // Agent type exists, but no Copilot Capability value and no RegisterCapability. + end; +} diff --git a/community/knowledge/agents/register-copilot-capability-for-the-agent.good.al b/community/knowledge/agents/register-copilot-capability-for-the-agent.good.al new file mode 100644 index 00000000..279b020a --- /dev/null +++ b/community/knowledge/agents/register-copilot-capability-for-the-agent.good.al @@ -0,0 +1,26 @@ +enumextension 50101 "Sales Review Agent Copilot" extends "Copilot Capability" +{ + value(50101; "Sales Review Agent") + { + Caption = 'Sales Review Agent'; + } +} + +codeunit 50101 "Sales Review Agent Install" +{ + Subtype = Install; + Access = Internal; + + trigger OnInstallAppPerDatabase() + var + CopilotCapability: Codeunit "Copilot Capability"; + LearnMoreUrlTxt: Label 'https://example.com/sales-review-agent', Locked = true; + begin + if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"Sales Review Agent") then + CopilotCapability.RegisterCapability( + Enum::"Copilot Capability"::"Sales Review Agent", + Enum::"Copilot Availability"::Preview, + "Copilot Billing Type"::"Microsoft Billed", + LearnMoreUrlTxt); + end; +} diff --git a/community/knowledge/agents/register-copilot-capability-for-the-agent.md b/community/knowledge/agents/register-copilot-capability-for-the-agent.md new file mode 100644 index 00000000..79dfe440 --- /dev/null +++ b/community/knowledge/agents/register-copilot-capability-for-the-agent.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [copilot-capability, registercapability, install, feature-switch, enumextension] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Register a Copilot capability for the agent on install + +## Description + +Each agent type needs a `Copilot Capability` enum value that the factory links as the feature switch and billing surface. The capability is invisible on Copilot and agent capabilities until an install codeunit calls `RegisterCapability` when it is not already registered. Unique ordinals matter across installed apps. Models often extend `Agent Metadata Provider` and never register the capability. + +## Best Practice + +Extend `Copilot Capability` with a unique value. In `OnInstallAppPerDatabase`, call `Copilot Capability.IsCapabilityRegistered` and, if false, `RegisterCapability` with availability, billing type, and a learn-more URL. Point `IAgentFactory` at that capability. + +See sample: `register-copilot-capability-for-the-agent.good.al`. + +## Anti Pattern + +Shipping the agent enum without a `Copilot Capability` value, or adding the enum but never calling `RegisterCapability`. Duplicate ordinals across extensions also collide. Detection signal: agent metadata provider with no matching capability registration in an install codeunit. + +See sample: `register-copilot-capability-for-the-agent.bad.al`. + +## See also + +`wire-all-three-agent-interfaces.md` covers registration of the provider implementation that references this capability. diff --git a/community/knowledge/agents/set-instructions-as-secrettext.bad.al b/community/knowledge/agents/set-instructions-as-secrettext.bad.al new file mode 100644 index 00000000..734f0b23 --- /dev/null +++ b/community/knowledge/agents/set-instructions-as-secrettext.bad.al @@ -0,0 +1,19 @@ +codeunit 50100 "Sales Review Agent Create" +{ + procedure CreateWithInstructions() + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + AgentUserSecurityId: Guid; + InstructionsLbl: Label 'You are a sales validation agent. Check credit.', Locked = true; + begin + AgentUserSecurityId := Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + // Label/text is not SecretText and is type-wide, not per instance. + Agent.SetInstructions(AgentUserSecurityId, InstructionsLbl); + Agent.Activate(AgentUserSecurityId); + end; +} diff --git a/community/knowledge/agents/set-instructions-as-secrettext.good.al b/community/knowledge/agents/set-instructions-as-secrettext.good.al new file mode 100644 index 00000000..8bedf697 --- /dev/null +++ b/community/knowledge/agents/set-instructions-as-secrettext.good.al @@ -0,0 +1,20 @@ +codeunit 50100 "Sales Review Agent Create" +{ + procedure CreateWithInstructions() + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + AgentUserSecurityId: Guid; + Instructions: SecretText; + InstructionsNameTxt: Label 'Instructions.txt', Locked = true; + begin + AgentUserSecurityId := Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + Instructions := NavApp.GetResourceAsText(InstructionsNameTxt); + Agent.SetInstructions(AgentUserSecurityId, Instructions); + Agent.Activate(AgentUserSecurityId); + end; +} diff --git a/community/knowledge/agents/set-instructions-as-secrettext.md b/community/knowledge/agents/set-instructions-as-secrettext.md new file mode 100644 index 00000000..0f246f6a --- /dev/null +++ b/community/knowledge/agents/set-instructions-as-secrettext.md @@ -0,0 +1,26 @@ +--- +bc-version: [27..] +domain: agents +keywords: [setinstructions, secrettext, instructions, per-instance, resource] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Set agent instructions as SecretText on the instance + +## Description + +Instructions are instance data, not an enum caption. `Agent.SetInstructions` takes `SecretText` so the payload is not logged or copied as ordinary text. A Label or plaintext Text on the agent type is the wrong store: it leaks into telemetry-friendly strings and cannot vary per instance or company. + +## Best Practice + +Load instruction text from a resource or builder into a `SecretText` variable and call `Agent.SetInstructions(AgentUserSecurityId, Instructions)` after `Create`. Keep one instruction document per instance. + +See sample: `set-instructions-as-secrettext.good.al`. + +## Anti Pattern + +Passing a `Label` or `Text` to `SetInstructions`, storing instructions in a setup Text field without wrapping as `SecretText`, or putting the prompt only in a code comment. Detection signal: `SetInstructions` with a non-`SecretText` argument, or no `SetInstructions` after `Create`. + +See sample: `set-instructions-as-secrettext.bad.al`. diff --git a/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.bad.al b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.bad.al new file mode 100644 index 00000000..570ca094 --- /dev/null +++ b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.bad.al @@ -0,0 +1,36 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure ShowCanCreateAgent(): Boolean + begin + // Author intends this to forbid all creates. It only hides the UI tile. + exit(false); + end; +} + +pageextension 50100 "Sales Order List Agent Create" extends "Sales Order List" +{ + actions + { + addlast(Processing) + { + action(CreateAgent) + { + ApplicationArea = All; + Caption = 'Create review agent'; + + trigger OnAction() + var + Agent: Codeunit Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + begin + // Still succeeds for any caller with permission to run this action. + Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + end; + } + } + } +} diff --git a/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.good.al b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.good.al new file mode 100644 index 00000000..4601f1d1 --- /dev/null +++ b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.good.al @@ -0,0 +1,25 @@ +codeunit 50100 "Sales Review Agent Factory" +{ + procedure ShowCanCreateAgent(): Boolean + var + AgentSystemPermissions: Codeunit "Agent System Permissions"; + begin + // Hides the type from non-admins in the UI. Does not block Agent.Create. + exit(AgentSystemPermissions.CurrentUserHasCanManageAllAgentsPermission()); + end; + + procedure CreateIfAllowed() + var + Agent: Codeunit Agent; + AgentSystemPermissions: Codeunit "Agent System Permissions"; + TempAgentAccessControl: Record "Agent Access Control" temporary; + begin + if not AgentSystemPermissions.CurrentUserHasCanManageAllAgentsPermission() then + Error('Only agent administrators can create this agent.'); + Agent.Create( + Enum::"Agent Metadata Provider"::"Sales Review Agent", + 'SALESREVIEW', + 'Sales Review Agent', + TempAgentAccessControl); + end; +} diff --git a/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.md b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.md new file mode 100644 index 00000000..1eb151d5 --- /dev/null +++ b/community/knowledge/agents/show-can-create-agent-does-not-block-code-create.md @@ -0,0 +1,26 @@ +--- +bc-version: [28..] +domain: agents +keywords: [showcancreateagent, agent-discovery, agent-create, administrator, agent-configuration-rights] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# ShowCanCreateAgent only hides UI create, not programmatic create + +## Description + +`IAgentFactory.ShowCanCreateAgent` controls whether the type appears in the in-client create UI. Returning false does not stop `Agent.Create` from AL. From 28.1, non-admins can discover extension agents unless this method (and agent configuration rights) restrict them. Models treat a false return as a hard create lock. + +## Best Practice + +Use `ShowCanCreateAgent` to decide discovery. If only agent administrators should see the type, return `Agent System Permissions.CurrentUserHasCanManageAllAgentsPermission`. Enforce extra policy inside your own create API. Never assume UI hiding blocks code. + +See sample: `show-can-create-agent-does-not-block-code-create.good.al`. + +## Anti Pattern + +Returning `exit(false)` from `ShowCanCreateAgent` and then documenting that instances cannot be created, while page actions or other apps still call `Agent.Create`. Detection signal: `ShowCanCreateAgent` always false with no matching guard on programmatic create. + +See sample: `show-can-create-agent-does-not-block-code-create.bad.al`. diff --git a/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.bad.al b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.bad.al new file mode 100644 index 00000000..8a184c68 --- /dev/null +++ b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.bad.al @@ -0,0 +1,15 @@ +codeunit 50100 "Sales Review Agent Tasks" +{ + procedure EnqueueFromEmailBody(RawEmailBody: Text; AgentUserSecurityId: Guid) + var + AgentTaskBuilder: Codeunit "Agent Task Builder"; + AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder"; + AgentTask: Record "Agent Task"; + begin + AgentTaskMessageBuilder.Initialize('Internet', RawEmailBody) + .SetRequiresReview(false); + AgentTask := AgentTaskBuilder.Initialize(AgentUserSecurityId, 'Process inbound mail') + .AddTaskMessage(AgentTaskMessageBuilder) + .Create(); + end; +} diff --git a/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.good.al b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.good.al new file mode 100644 index 00000000..394aeb27 --- /dev/null +++ b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.good.al @@ -0,0 +1,16 @@ +codeunit 50100 "Sales Review Agent Tasks" +{ + procedure EnqueueFromSalesOrder(SalesHeader: Record "Sales Header"; AgentUserSecurityId: Guid) + var + AgentTaskBuilder: Codeunit "Agent Task Builder"; + AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder"; + AgentTask: Record "Agent Task"; + begin + SalesHeader.TestField("No."); + AgentTaskMessageBuilder.Initialize('Sales Team', 'Review sales order ' + SalesHeader."No.") + .SetRequiresReview(false); + AgentTask := AgentTaskBuilder.Initialize(AgentUserSecurityId, 'Review Sales Order') + .AddTaskMessage(AgentTaskMessageBuilder) + .Create(); + end; +} diff --git a/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.md b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.md new file mode 100644 index 00000000..449038f0 --- /dev/null +++ b/community/knowledge/agents/skip-incoming-review-only-for-trusted-input.md @@ -0,0 +1,26 @@ +--- +bc-version: [28..] +domain: agents +keywords: [setrequiresreview, agent-task-message-builder, approval, trusted-input, skip-review] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Skip incoming message review only after the caller validated the payload + +## Description + +Incoming task messages default to requiring user approval before the agent runs. From 28.1, `Agent Task Message Builder.SetRequiresReview(false)` starts the agent immediately. That is safe only for inputs you already validated in AL (your page action, your posting subscriber). External email or partner payloads are not trusted by default. Analysis Warnings still force a review. + +## Best Practice + +Leave the default review-on for anything that originated outside your extension. Call `SetRequiresReview(false)` only on messages you constructed from already-authorized BC data. + +See sample: `skip-incoming-review-only-for-trusted-input.good.al`. + +## Anti Pattern + +`SetRequiresReview(false)` on simulated email, incoming webhooks, or user-free text. Detection signal: `SetRequiresReview(false)` next to external content with no prior validation. + +See sample: `skip-incoming-review-only-for-trusted-input.bad.al`. diff --git a/community/knowledge/agents/use-documented-instruction-keywords.bad.al b/community/knowledge/agents/use-documented-instruction-keywords.bad.al new file mode 100644 index 00000000..0854a3de --- /dev/null +++ b/community/knowledge/agents/use-documented-instruction-keywords.bad.al @@ -0,0 +1,7 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + begin + Instructions := 'When done, email the customer and remember the credit limit. Click Post_Promoted.'; + end; +} diff --git a/community/knowledge/agents/use-documented-instruction-keywords.good.al b/community/knowledge/agents/use-documented-instruction-keywords.good.al new file mode 100644 index 00000000..e03adeea --- /dev/null +++ b/community/knowledge/agents/use-documented-instruction-keywords.good.al @@ -0,0 +1,13 @@ +codeunit 50100 "Sales Review Agent Instr." +{ + procedure GetInstructions() Instructions: SecretText + var + Builder: TextBuilder; + begin + Builder.AppendLine('When the sales order is ready, request a review before posting.'); + Builder.AppendLine('If a field is missing, ask for assistance.'); + Builder.AppendLine('Memorize the customer credit limit for later steps.'); + Builder.AppendLine('When confirmed, write an email to the salesperson; outbound mail is reviewed.'); + Instructions := Builder.ToText(); + end; +} diff --git a/community/knowledge/agents/use-documented-instruction-keywords.md b/community/knowledge/agents/use-documented-instruction-keywords.md new file mode 100644 index 00000000..5e4ffaab --- /dev/null +++ b/community/knowledge/agents/use-documented-instruction-keywords.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [instruction-keywords, request-a-review, memorize, write-an-email, invoke-action] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Use the toolkit instruction keywords for review, mail, and memory + +## Description + +The agent runtime looks for specific phrases: ask for assistance, request a review, reply, write an email, memorize, set a field, use lookup, invoke an action. Ordinary English such as get a human to look or remember this is weaker. Outbound reply and email always require review; that is platform policy, not optional tone. + +## Best Practice + +In the instruction resource, use those keywords at the decision points: request a review before posting; write an email only after stating that outbound mail is reviewed; memorize values the later steps need. Pair `Reply` / `Write an email` with an explicit review sentence. + +See sample: `use-documented-instruction-keywords.good.al`. + +## Anti Pattern + +Inventing tool-like verbs (call Copilot, click Post_Promoted) or omitting request a review before posting. Detection signal: instruction text that says email the customer with no review keyword. + +See sample: `use-documented-instruction-keywords.bad.al`. + +## See also + +`instruction-structure-is-role-rules-steps.md` defines the containing document structure, while `instructions-describe-work-not-tool-ids.md` keeps outcomes independent of UI tool identifiers. diff --git a/community/knowledge/agents/wire-all-three-agent-interfaces.bad.al b/community/knowledge/agents/wire-all-three-agent-interfaces.bad.al new file mode 100644 index 00000000..e4d4bcb4 --- /dev/null +++ b/community/knowledge/agents/wire-all-three-agent-interfaces.bad.al @@ -0,0 +1,9 @@ +enumextension 50100 "Sales Review Agent Metadata" extends "Agent Metadata Provider" +{ + value(50100; "Sales Review Agent") + { + Caption = 'Sales Review Agent'; + // Only factory is bound. Metadata UI and task execution never resolve. + Implementation = IAgentFactory = "Sales Review Agent Factory"; + } +} diff --git a/community/knowledge/agents/wire-all-three-agent-interfaces.good.al b/community/knowledge/agents/wire-all-three-agent-interfaces.good.al new file mode 100644 index 00000000..3d8719ee --- /dev/null +++ b/community/knowledge/agents/wire-all-three-agent-interfaces.good.al @@ -0,0 +1,10 @@ +enumextension 50100 "Sales Review Agent Metadata" extends "Agent Metadata Provider" +{ + value(50100; "Sales Review Agent") + { + Caption = 'Sales Review Agent'; + Implementation = IAgentFactory = "Sales Review Agent Factory", + IAgentMetadata = "Sales Review Agent Metadata", + IAgentTaskExecution = "Sales Review Agent Task"; + } +} diff --git a/community/knowledge/agents/wire-all-three-agent-interfaces.md b/community/knowledge/agents/wire-all-three-agent-interfaces.md new file mode 100644 index 00000000..d3ce4b2b --- /dev/null +++ b/community/knowledge/agents/wire-all-three-agent-interfaces.md @@ -0,0 +1,30 @@ +--- +bc-version: [27..] +domain: agents +keywords: [agent-metadata-provider, iagentfactory, iagentmetadata, iagenttaskexecution, enumextension, implementation] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Wire all three agent interfaces on the metadata provider + +## Description + +An AL agent type is registered by extending `Agent Metadata Provider`. The platform locates factory, metadata, and task-execution behaviour only through the `Implementation` property on that enum value. Omitting `IAgentFactory`, `IAgentMetadata`, or `IAgentTaskExecution` leaves create, UI identity, or task runs unbound. Models often ship a single codeunit and skip the enum wiring. + +## Best Practice + +On the enum value, set `Implementation` for all three interfaces, each pointing at a dedicated codeunit. Keep factory (create, defaults, first-time setup), metadata (setup page, summary, annotations), and task execution (message analysis, intervention suggestions) in separate objects. + +See sample: `wire-all-three-agent-interfaces.good.al`. + +## Anti Pattern + +An `Agent Metadata Provider` value with no `Implementation`, only one interface mapped, or all three interfaces pointing at one catch-all codeunit that cannot satisfy the contracts. Detection signal: enumextension of `Agent Metadata Provider` whose value does not list `IAgentFactory`, `IAgentMetadata`, and `IAgentTaskExecution`. + +See sample: `wire-all-three-agent-interfaces.bad.al`. + +## See also + +`register-copilot-capability-for-the-agent.md` covers the feature capability linked by the factory implementation. diff --git a/community/skills/review/al-agents-review.md b/community/skills/review/al-agents-review.md new file mode 100644 index 00000000..c63e3712 --- /dev/null +++ b/community/skills/review/al-agents-review.md @@ -0,0 +1,68 @@ +--- +kind: action-skill +id: al-agents-review +version: 1 +title: AL agents review +description: Reviews AL source changes against agent guidance from BCQuality. +inputs: [pr-diff, file-path] +outputs: [findings-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL agents review + +Reviews AL source changes against the `agents` knowledge domain in BCQuality and emits a findings report. This is a leaf action skill: it invokes no sub-skills. + +An orchestrator invokes this skill with either a `pr-diff` or a `file-path`. The skill produces one JSON document conforming to the DO output contract. + +## Source + +Read the root `knowledge-index.json` generated by Entry and select entries whose `domain` is `agents` across every enabled layer. Use index metadata for candidate selection and open an article body only after it enters the worklist. + +## Relevance + +Apply READ's frontmatter matching semantics to the target BC version, AL technology, countries, and application areas. If a dimension is unknown, retain conditionally applicable guidance only when configuration permits it; cap resulting confidence at `medium` and name the unknown dimension in the finding message. + +## Worklist + +Match changed objects, procedures, interfaces, and tokens against article keywords, titles, descriptions, and paths. Give particular weight to: + +- Implementations of `IAgentFactory`, `IAgentMetadata`, and `IAgentTaskExecution`. +- Agent setup tables and `ConfigurationDialog` pages using `Agent Setup`, `Agent Setup Buffer`, or `Agent Setup Part`. +- Agent creation, upgrade, capability registration, profile configuration, access controls, and subscriber binding. +- Instruction construction, `SecretText`, documented instruction keywords, task messages, trusted input, warnings, errors, and review behavior. +- Public APIs invoked by agent tasks across app boundaries. + +Use these targeted rules to avoid broad token-only matches: + +- Worklist setup-page shape guidance when the page returned by agent metadata is not a `ConfigurationDialog` or omits `Agent Setup Part`. +- Worklist temporary-source guidance when setup writes occur before a non-Cancel close path or a setup page is not temporary. +- Worklist permission guidance when default access controls are broad or when code assumes an agent can exceed the assigning user's permissions. +- Worklist instruction guidance only for text used as agent instructions; do not flag unrelated prompts, labels, or user-facing help. +- Worklist session-binding guidance only when subscribers are bound outside an agent session or left bound after execution. + +After selection, resolve conflicting guidance using READ's layer precedence. Record displaced candidates in `suppressed` with `reason: "layer-precedence"`; record disabled-layer candidates with `reason: "configuration"`. + +An empty worklist caused by absent applicable knowledge produces `no-knowledge`. An empty worklist caused by no match produces `completed` with no findings. + +## Action + +Evaluate each worklisted article's `## Best Practice` and `## Anti Pattern` against the changed code: + +- Emit `major` for a clear anti-pattern and `minor` for a concrete best-practice contradiction. +- Use `blocker` only when the article identifies a violated platform guarantee. +- Do not emit a finding from applicability alone. +- Set confidence to `high` for unambiguous syntax or identifier evidence, `medium` for heuristic or conditionally applicable evidence, and `low` only for an explicit advisory. + +Agent-originated findings without a matching article must follow the DO contract: prefix the ID with `agent:`, use `references: []`, cap severity at `minor` and confidence at `medium`, and emit only concrete defects within the agents domain. + +Provide `suggested-code` when the repair is small, local, and unambiguous. Otherwise, when a mechanical-looking repair depends on missing context or has multiple valid forms, set `suggested-code-omission-reason`. + +Use the standard DO outcomes: `completed`, `no-knowledge`, `not-applicable`, `partial`, or `failed`. + +## Output + +Return only one JSON document conforming to the DO output contract. Every finding emitted by this skill MUST set `findings[].domain` to `"Agents"`. Knowledge-backed finding IDs and references MUST use the exact repository-relative article path from the knowledge index. \ No newline at end of file diff --git a/evaluation/README.md b/evaluation/README.md index cd25d3db..7063bafa 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -1,6 +1,6 @@ # AL review evaluation -The evaluation is convention-driven. For every `microsoft/skills/review/al--review.md` leaf, the harness finds `microsoft/knowledge//`, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit. +The evaluation is convention-driven. The harness discovers every `/skills/review/al--review.md` leaf across the enabled `microsoft`, `community`, and `custom` layers. Duplicate domains resolve with `custom > community > microsoft` precedence. For each selected leaf, the harness finds paired knowledge across the same layers, applies the same precedence to duplicate article slugs, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit. `review-fixtures.json` contains only global thresholds and optional exceptional overrides. An override may select a different article or add context when the generic convention cannot express a scenario. It should remain empty in the normal case. @@ -12,7 +12,7 @@ Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name t pwsh ./tools/Test-ReviewFixtures.ps1 -Root . ``` -This credential-free check proves every registered leaf maps to a same-named knowledge domain with at least one complete AL sample pair and that all configured overrides are valid. +This credential-free check proves every selected leaf maps to a same-named knowledge domain with at least one complete AL sample pair and that all configured overrides are valid. ## Run a fast-model evaluation @@ -26,7 +26,7 @@ This credential-free check proves every registered leaf maps to a same-named kno 2. For a fast/small model, use one fresh invocation per `request-case-*.json`. Each request embeds the exact leaf instructions, that domain's candidate index rows with authoritative paths, and one opaque case. The model opens only matching articles and copies finding IDs from `candidateArticles[].path`. Save each response with the matching `result-case-*.json` name in the same directory. - `request-.json` files provide optional two-case leaf batches; save those as `result-.json`. Directory scoring prefers `result-case-*.json` when present and otherwise falls back to `result-*.json`. `review-request.json` is an optional all-domains stress test for larger models. Neither batch form is the preferred fast-model profile. + `request-.json` files provide optional two-case leaf batches and identify the selected layer-owned skill path; save those as `result-.json`. Directory scoring prefers `result-case-*.json` when present and otherwise falls back to `result-*.json`. `review-request.json` is an optional all-domains stress test for larger models. Neither batch form is the preferred fast-model profile. 3. Save only this result shape: diff --git a/tools/Test-ReviewFixtures.ps1 b/tools/Test-ReviewFixtures.ps1 index 3f3f1444..124b6336 100644 --- a/tools/Test-ReviewFixtures.ps1 +++ b/tools/Test-ReviewFixtures.ps1 @@ -109,12 +109,44 @@ if (([double]$manifest.minimumCleanRate -lt 0) -or ([double]$manifest.minimumCle $problems.Add('minimumCleanRate must be between 0 and 1.') | Out-Null } -$leafDomains = @( - Get-ChildItem -LiteralPath (Join-Path $Root 'microsoft/skills/review') -File -Filter 'al-*-review.md' | - Where-Object Name -ne 'al-code-review.md' | - ForEach-Object { $_.BaseName -replace '^al-', '' -replace '-review$', '' } | - Sort-Object -Unique +$layers = @( + [pscustomobject]@{ Name = 'microsoft'; Rank = 1 } + [pscustomobject]@{ Name = 'community'; Rank = 2 } + [pscustomobject]@{ Name = 'custom'; Rank = 3 } ) +$layerRanks = @{} +foreach ($layer in $layers) { + $layerRanks[[string]$layer.Name] = [int]$layer.Rank +} +$leafCandidates = @( + foreach ($layer in $layers) { + $reviewDirectory = Join-Path $Root "$($layer.Name)/skills/review" + if (-not (Test-Path -LiteralPath $reviewDirectory -PathType Container)) { + continue + } + Get-ChildItem -LiteralPath $reviewDirectory -File -Filter 'al-*-review.md' | + Where-Object Name -ne 'al-code-review.md' | + ForEach-Object { + [pscustomobject]@{ + Domain = $_.BaseName -replace '^al-', '' -replace '-review$', '' + Layer = $layer.Name + Rank = $layer.Rank + RelativePath = [System.IO.Path]::GetRelativePath($Root, $_.FullName).Replace('\', '/') + } + } + } +) +$leafSkills = @( + $leafCandidates | + Group-Object Domain | + ForEach-Object { $_.Group | Sort-Object Rank -Descending | Select-Object -First 1 } | + Sort-Object Domain +) +$leafDomains = @($leafSkills | ForEach-Object Domain) +$leafByDomain = @{} +foreach ($leafSkill in $leafSkills) { + $leafByDomain[[string]$leafSkill.Domain] = $leafSkill +} $overrides = @{} if ($manifest.PSObject.Properties.Name -contains 'overrides') { @@ -130,9 +162,35 @@ foreach ($overrideDomain in $overrides.Keys) { $caseList = [System.Collections.Generic.List[object]]::new() foreach ($domain in $leafDomains) { - $knowledgeDirectory = Join-Path $Root "microsoft/knowledge/$domain" - if (-not (Test-Path -LiteralPath $knowledgeDirectory -PathType Container)) { - $problems.Add("${domain}: no Microsoft knowledge directory exists.") | Out-Null + $articleCandidates = @( + foreach ($layer in $layers) { + $knowledgeDirectory = Join-Path $Root "$($layer.Name)/knowledge/$domain" + if (-not (Test-Path -LiteralPath $knowledgeDirectory -PathType Container)) { + continue + } + Get-ChildItem -LiteralPath $knowledgeDirectory -File -Filter '*.md' | + Where-Object { + (Test-Path -LiteralPath (Join-Path $knowledgeDirectory "$($_.BaseName).good.al") -PathType Leaf) -and + (Test-Path -LiteralPath (Join-Path $knowledgeDirectory "$($_.BaseName).bad.al") -PathType Leaf) + } | + ForEach-Object { + [pscustomobject]@{ + BaseName = $_.BaseName + File = $_ + Rank = $layer.Rank + ArticlePath = [System.IO.Path]::GetRelativePath($Root, $_.FullName).Replace('\', '/') + } + } + } + ) + $articles = @( + $articleCandidates | + Group-Object BaseName | + ForEach-Object { $_.Group | Sort-Object Rank -Descending | Select-Object -First 1 } | + Sort-Object BaseName + ) + if (-not $articles.Count) { + $problems.Add("${domain}: no enabled knowledge layer has an article with both .good.al and .bad.al companion samples.") | Out-Null continue } @@ -143,27 +201,20 @@ foreach ($domain in $leafDomains) { if ($articleName.EndsWith('.md')) { $articleName = [System.IO.Path]::GetFileNameWithoutExtension($articleName) } - $candidate = Join-Path $knowledgeDirectory "$articleName.md" - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $selectedArticle = Get-Item -LiteralPath $candidate - } else { + $selectedArticle = $articles | Where-Object BaseName -eq $articleName | Select-Object -First 1 + if (-not $selectedArticle) { $problems.Add("${domain}: override article does not exist: $articleName.md") | Out-Null } } else { - $selectedArticle = Get-ChildItem -LiteralPath $knowledgeDirectory -File -Filter '*.md' | - Sort-Object Name | - Where-Object { - (Test-Path -LiteralPath (Join-Path $knowledgeDirectory "$($_.BaseName).good.al") -PathType Leaf) -and - (Test-Path -LiteralPath (Join-Path $knowledgeDirectory "$($_.BaseName).bad.al") -PathType Leaf) - } | - Select-Object -First 1 + $selectedArticle = $articles | Select-Object -First 1 } if (-not $selectedArticle) { $problems.Add("${domain}: no article has both .good.al and .bad.al companion samples.") | Out-Null continue } - $articlePath = "microsoft/knowledge/$domain/$($selectedArticle.Name)" + $articlePath = [string]$selectedArticle.ArticlePath + $sampleDirectory = Split-Path -Parent $articlePath $context = if ($override -and ($override.PSObject.Properties.Name -contains 'context')) { [string]$override.context } else { @@ -173,7 +224,7 @@ foreach ($domain in $leafDomains) { $case = [pscustomobject]@{ id = "$domain-$kind" domain = $domain - input = "microsoft/knowledge/$domain/$($selectedArticle.BaseName).$kind.al" + input = "$sampleDirectory/$($selectedArticle.BaseName).$kind.al" expected = if ($kind -eq 'bad') { @($articlePath) } else { @() } } if ($context) { @@ -188,7 +239,7 @@ $seenIds = [System.Collections.Generic.HashSet[string]]::new([System.StringCompa foreach ($case in $cases) { $id = [string]$case.id $domain = [string]$case.domain - $input = [string]$case.input + $inputRelativePath = [string]$case.input $expected = @($case.expected) if ([string]::IsNullOrWhiteSpace($id)) { @@ -200,15 +251,15 @@ foreach ($case in $cases) { $problems.Add("${id}: domain '$domain' has no registered al-$domain-review leaf.") | Out-Null } - $inputPath = Join-Path $Root $input + $inputPath = Join-Path $Root $inputRelativePath if (-not (Test-Path -LiteralPath $inputPath -PathType Leaf)) { - $problems.Add("${id}: input does not exist: $input") | Out-Null + $problems.Add("${id}: input does not exist: $inputRelativePath") | Out-Null } - if ($expected.Count -and $input -notmatch '\.bad\.[^.]+$') { - $problems.Add("${id}: positive case must use a .bad sample: $input") | Out-Null + if ($expected.Count -and $inputRelativePath -notmatch '\.bad\.[^.]+$') { + $problems.Add("${id}: positive case must use a .bad sample: $inputRelativePath") | Out-Null } - if (-not $expected.Count -and $input -notmatch '\.good\.[^.]+$') { - $problems.Add("${id}: clean case must use a .good sample: $input") | Out-Null + if (-not $expected.Count -and $inputRelativePath -notmatch '\.good\.[^.]+$') { + $problems.Add("${id}: clean case must use a .good sample: $inputRelativePath") | Out-Null } foreach ($reference in $expected) { @@ -218,7 +269,7 @@ foreach ($case in $cases) { } } if ($expected.Count) { - $sampleSlug = ([System.IO.Path]::GetFileName($input) -replace '\.(?:good|bad)\.[^.]+$', '') + $sampleSlug = ([System.IO.Path]::GetFileName($inputRelativePath) -replace '\.(?:good|bad)\.[^.]+$', '') $primarySlug = [System.IO.Path]::GetFileNameWithoutExtension([string]$expected[0]) if ($sampleSlug -ne $primarySlug) { $problems.Add("${id}: primary expected article '$primarySlug' must match sample slug '$sampleSlug'.") | Out-Null @@ -311,9 +362,16 @@ if ($PrepareDirectory) { } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $PrepareDirectory 'review-request.json') -Encoding UTF8 foreach ($domain in $leafDomains) { - $domainArticles = @($fullIndex.articles | Where-Object domain -eq $domain) + $domainArticles = @( + $fullIndex.articles | + Where-Object domain -eq $domain | + Sort-Object @{ Expression = { $layerRanks[[string]$_.layer] }; Descending = $true }, path | + Group-Object { [System.IO.Path]::GetFileName([string]$_.path) } | + ForEach-Object { $_.Group | Select-Object -First 1 } | + Sort-Object path + ) $domainIndexName = "index-$domain.json" - $leafPath = "microsoft/skills/review/al-$domain-review.md" + $leafPath = [string]$leafByDomain[$domain].RelativePath $leafFullText = Get-Content -LiteralPath (Join-Path $Root $leafPath) -Raw $leafInstructions = @($leafFullText -split '(?m)^## Output\s*\r?\n', 2)[0] $leafInstructions += "`n## Output`nReturn only the request's resultSchema." From 38d341eabb7a3ed4b4a4dacd16d8adf33102df20 Mon Sep 17 00:00:00 2001 From: Stefano Demiliani Date: Mon, 24 Aug 2026 15:31:27 +0200 Subject: [PATCH 2/2] fix(community/agents): align setup and permission samples - mark agent setup pages as non-extensible where required - narrow the agent profile by hiding an unrelated sales-order field - define a dedicated read-only permission set for the sales review agent - assign AL-defined permission sets with system scope and the owning app ID - clarify the permission scope guidance for default access controls --- .../agents/agent-profile-narrows-visible-ui.good.al | 8 ++++++++ .../agent-setup-page-is-configuration-dialog.good.al | 1 + .../agent-setup-page-is-configuration-dialog.md | 1 + .../agent-setup-source-table-is-temporary.good.al | 1 + ...te-agents-in-install-upgrade-or-background.good.al | 1 + ...et-default-access-controls-least-privilege.good.al | 11 ++++++++++- .../get-default-access-controls-least-privilege.md | 2 +- 7 files changed, 23 insertions(+), 2 deletions(-) diff --git a/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al b/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al index 9a3e52a3..5ffe61a1 100644 --- a/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al +++ b/community/knowledge/agents/agent-profile-narrows-visible-ui.good.al @@ -8,6 +8,14 @@ profile "SALES REVIEW AGENT" pagecustomization "Sales Review Agent Sales Ord." customizes "Sales Order" { + layout + { + modify("Payment Terms Code") + { + Visible = false; + } + } + actions { modify(Post) diff --git a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al index fb76031c..6c4eaf24 100644 --- a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al +++ b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.good.al @@ -4,6 +4,7 @@ page 50100 "Sales Review Agent Setup" Caption = 'Set up Sales Review Agent'; SourceTable = "Sales Review Agent Setup"; SourceTableTemporary = true; + Extensible = false; layout { diff --git a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md index f3460103..f13f5c0f 100644 --- a/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md +++ b/community/knowledge/agents/agent-setup-page-is-configuration-dialog.md @@ -16,6 +16,7 @@ Instance setup is not a Card or StandardDialog. The toolkit expects `PageType = ## Best Practice Declare `PageType = ConfigurationDialog`, host `part(...; "Agent Setup Part")`, and put agent-specific fields in another group. Keep system OK/Cancel. Use a temporary source record and defer persistence until Update, as described in `agent-setup-source-table-is-temporary.md`. +The Extensible property of the page must be set to false. See sample: `agent-setup-page-is-configuration-dialog.good.al`. diff --git a/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al b/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al index 7326c925..cbeabca8 100644 --- a/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al +++ b/community/knowledge/agents/agent-setup-source-table-is-temporary.good.al @@ -3,6 +3,7 @@ page 50100 "Sales Review Agent Setup" PageType = ConfigurationDialog; SourceTable = "Sales Review Agent Setup"; SourceTableTemporary = true; + Extensible = false; layout { diff --git a/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al index 198f0afc..508b7989 100644 --- a/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al +++ b/community/knowledge/agents/do-not-create-agents-in-install-upgrade-or-background.good.al @@ -4,6 +4,7 @@ page 50100 "Sales Review Agent Setup" ApplicationArea = All; SourceTable = "Sales Review Agent Setup"; SourceTableTemporary = true; + Extensible = false; layout { diff --git a/community/knowledge/agents/get-default-access-controls-least-privilege.good.al b/community/knowledge/agents/get-default-access-controls-least-privilege.good.al index e3e6716e..2c855f7c 100644 --- a/community/knowledge/agents/get-default-access-controls-least-privilege.good.al +++ b/community/knowledge/agents/get-default-access-controls-least-privilege.good.al @@ -1,3 +1,12 @@ +permissionset 50100 "SALES REVIEW AGENT" +{ + Assignable = true; + Caption = 'Sales Review Agent'; + Permissions = + tabledata "Sales Header" = R, + tabledata "Sales Line" = R; +} + codeunit 50100 "Sales Review Agent Factory" { procedure GetDefaultAccessControls(var TempAccessControlBuffer: Record "Access Control Buffer" temporary) @@ -8,7 +17,7 @@ codeunit 50100 "Sales Review Agent Factory" NavApp.GetCurrentModuleInfo(CurrentModuleInfo); Clear(TempAccessControlBuffer); TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name")); - TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::Tenant; + TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::System; TempAccessControlBuffer."App ID" := CurrentModuleInfo.Id; TempAccessControlBuffer."Role ID" := RoleIdTok; TempAccessControlBuffer.Insert(); diff --git a/community/knowledge/agents/get-default-access-controls-least-privilege.md b/community/knowledge/agents/get-default-access-controls-least-privilege.md index c0e87b41..87349af8 100644 --- a/community/knowledge/agents/get-default-access-controls-least-privilege.md +++ b/community/knowledge/agents/get-default-access-controls-least-privilege.md @@ -15,7 +15,7 @@ application-area: [all] ## Best Practice -Insert only the permission sets the agent needs, with the correct Scope and App ID. Recreate any BC-only sets as AL permissionset objects first. Prefer a dedicated permission set over a full-user role. +Insert only the permission sets the agent needs. For an AL `permissionset` object, use `Scope::System` and the ID of the app that defines it. Recreate permission sets that exist only as user-defined configuration in Business Central as AL objects first. Prefer a dedicated permission set over a full-user role. See sample: `get-default-access-controls-least-privilege.good.al`.