From bf75bebdef68a5453429c429f5cd0aa7b452c4f7 Mon Sep 17 00:00:00 2001 From: Thaddeus Loke <39045749+thloke@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:24:58 +0000 Subject: [PATCH] [Agent Archiving] Payables and Expense agent implementation for IAgentArchiving (#11040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why Honours the new `IAgentArchiving` interface, and opts Payables Agent and Expense Agent out of archiving. The Archive action is disabled on the Agent List and Agent Card for agent types that do not support archiving, and `Agent.Archive` blocks it programmatically so the guard cannot be bypassed. The check runs off the record the pages already hold, so it costs no extra read and cannot fail on an unsaved record. Payables and Expense opt out via their own `PA Agent Archiving` / `EA Agent Archiving` codeunits, mirroring how each app already registers `IAgentTaskExecution`, so they can be removed cleanly once each agent supports archiving. Both are single-instance: the setup record enforcing that is left behind by archiving, which would block creating a replacement. Behaviour is unchanged for every agent type that does not opt out. ## Linked work [AB#647379](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/647379) ## Related PRs Cross-repo change - merge in this order: 1. **BC-Platform** - the interface: https://microsoft.ghe.com/bic/BC-Platform/pull/46005 2. **BCApps** - System App honours it, Payables and Expense opt out (this PR): https://github.com/microsoft/BCApps/pull/10460 > Note: this PR will not build until 1 is merged and a platform version carrying `IAgentArchiving` flows through. --------- Co-authored-by: Balázs Krupinszki Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Copilot-Session: 9724257d-43d1-468b-9f97-1fb863d2fdd1 --- .../Codeunits/EAAgentArchiving.Codeunit.al | 20 +++++++++++ .../EnumExtensions/EAAgentMetadata.EnumExt.al | 2 +- .../Integration/PAAgentArchiving.Codeunit.al | 20 +++++++++++ .../app/PAAgentMetadata.EnumExt.al | 2 +- .../App/Agent/Setup/Agent.Codeunit.al | 14 ++++++++ .../App/Agent/Setup/AgentCard.Page.al | 6 +++- .../App/Agent/Setup/AgentImpl.Codeunit.al | 25 ++++++++++++++ .../App/Agent/Setup/AgentList.Page.al | 6 +++- .../Test/Agent/src/SDK/AgentTest.Codeunit.al | 33 +++++++++++++++++++ .../src/Setup/LibraryMockAgent.codeunit.al | 2 +- .../Integration/MockAgentMetaProv.Codeunit.al | 7 +++- .../Integration/MockAgentMetaProv.EnumExt.al | 6 ++++ 12 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/EAAgentArchiving.Codeunit.al create mode 100644 src/Apps/W1/PayablesAgent/app/Integration/PAAgentArchiving.Codeunit.al diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/EAAgentArchiving.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/EAAgentArchiving.Codeunit.al new file mode 100644 index 00000000000..25d818bf06e --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/EAAgentArchiving.Codeunit.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.ExpenseAgent; + +using System.Agents; + +codeunit 7104 "EA Agent Archiving" implements IAgentArchiving +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure IsArchivingSupported(): Boolean + begin + exit(false); + end; +} diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/EnumExtensions/EAAgentMetadata.EnumExt.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/EnumExtensions/EAAgentMetadata.EnumExt.al index cdb7dfefee6..061d575a942 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/EnumExtensions/EAAgentMetadata.EnumExt.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/EnumExtensions/EAAgentMetadata.EnumExt.al @@ -13,6 +13,6 @@ enumextension 6998 "EA Agent Metadata" extends "Agent Metadata Provider" value(6998; "Expense Agent") { Caption = 'Expense Agent', Locked = true; - Implementation = IAgentFactory = "EA Metadata Provider", IAgentMetadata = "EA Metadata Provider", IAgentTaskExecution = "EA Agent Task Execution"; + Implementation = IAgentFactory = "EA Metadata Provider", IAgentMetadata = "EA Metadata Provider", IAgentTaskExecution = "EA Agent Task Execution", IAgentArchiving = "EA Agent Archiving"; } } diff --git a/src/Apps/W1/PayablesAgent/app/Integration/PAAgentArchiving.Codeunit.al b/src/Apps/W1/PayablesAgent/app/Integration/PAAgentArchiving.Codeunit.al new file mode 100644 index 00000000000..36fe3bef3d5 --- /dev/null +++ b/src/Apps/W1/PayablesAgent/app/Integration/PAAgentArchiving.Codeunit.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.PayablesAgent; + +using System.Agents; + +codeunit 3319 "PA Agent Archiving" implements IAgentArchiving +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure IsArchivingSupported(): Boolean + begin + exit(false); + end; +} diff --git a/src/Apps/W1/PayablesAgent/app/PAAgentMetadata.EnumExt.al b/src/Apps/W1/PayablesAgent/app/PAAgentMetadata.EnumExt.al index 3a6b1496e0c..0ea2395884e 100644 --- a/src/Apps/W1/PayablesAgent/app/PAAgentMetadata.EnumExt.al +++ b/src/Apps/W1/PayablesAgent/app/PAAgentMetadata.EnumExt.al @@ -13,6 +13,6 @@ enumextension 3304 "PA Agent Metadata" extends "Agent Metadata Provider" value(3303; "Payables Agent") { Caption = 'Payables Agent', Locked = true; - Implementation = IAgentFactory = "Payables Agent", IAgentMetadata = "Payables Agent", IAgentTaskExecution = "PA Agent Task Execution"; + Implementation = IAgentFactory = "Payables Agent", IAgentMetadata = "Payables Agent", IAgentTaskExecution = "PA Agent Task Execution", IAgentArchiving = "PA Agent Archiving"; } } diff --git a/src/System Application/App/Agent/Setup/Agent.Codeunit.al b/src/System Application/App/Agent/Setup/Agent.Codeunit.al index 4f882e0b9e9..b802c4d826b 100644 --- a/src/System Application/App/Agent/Setup/Agent.Codeunit.al +++ b/src/System Application/App/Agent/Setup/Agent.Codeunit.al @@ -61,6 +61,7 @@ codeunit 4321 Agent /// Archives the agent. Archiving removes the agent from active use and cannot be undone. The agent must be inactive (deactivated) before it can be archived. /// /// The user security ID of the agent. + /// An error is raised if the agent's type does not support archiving. Call IsArchivingSupported to check before archiving. procedure Archive(AgentUserSecurityID: Guid) var AgentImpl: Codeunit "Agent Impl."; @@ -69,6 +70,19 @@ codeunit 4321 Agent AgentImpl.Archive(AgentUserSecurityID); end; + /// + /// Checks if the agent type supports archiving. + /// + /// The user security ID of the agent. + /// True if the agent type supports archiving; otherwise false. + procedure IsArchivingSupported(AgentUserSecurityID: Guid): Boolean + var + AgentImpl: Codeunit "Agent Impl."; + begin + FeatureAccessManagement.AgentManagementAllowed(true); + exit(AgentImpl.IsArchivingSupported(AgentUserSecurityID)); + end; + /// /// Checks if the agent is archived. /// diff --git a/src/System Application/App/Agent/Setup/AgentCard.Page.al b/src/System Application/App/Agent/Setup/AgentCard.Page.al index c4d8987ec25..ec0257e8a96 100644 --- a/src/System Application/App/Agent/Setup/AgentCard.Page.al +++ b/src/System Application/App/Agent/Setup/AgentCard.Page.al @@ -151,6 +151,9 @@ page 4315 "Agent Card" Agent: Codeunit Agent; ArchiveConfirmation: Page "Agent Archive Confirmation"; begin + if not Agent.IsArchivingSupported(Rec."User Security ID") then + Error(ArchivingNotSupportedErr, Rec."Agent Metadata Provider"); + if Rec.State <> Rec.State::Disabled then Error(DeactivateBeforeArchivingErr); @@ -324,6 +327,7 @@ page 4315 "Agent Card" YouCannotEnableAgentWithoutUsingConfigurationPageErr: Label 'You can''t activate the agent from this page. Use the action to configure and activate the agent.'; YouDoNotHavePermissionToModifyThisAgentErr: Label 'You do not have permission to modify this agent. Contact your system administrator to update your permissions or to mark you as one of the administrators for the agent.'; DeactivateBeforeArchivingErr: Label 'Deactivate the agent before archiving it.'; + ArchivingNotSupportedErr: Label 'Archiving agents of type ''%1'' is not supported.', Comment = '%1 = the type of the agent.'; AgentArchivedMsg: Label 'The agent has been archived.'; AgentArchivedNotificationMsg: Label 'This agent is archived and can no longer be modified. Its tasks and logs remain available for auditing.'; -} \ No newline at end of file +} diff --git a/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al b/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al index 8b14bb3c40a..3c60ce14129 100644 --- a/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al +++ b/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al @@ -67,6 +67,9 @@ codeunit 4301 "Agent Impl." if Agent.Substate = Agent.Substate::Archived then exit; // Archiving is terminal; idempotent no-op avoids the platform "archived agent cannot be modified" error on re-archive. + if not IsArchivingSupported(Agent) then + Error(ArchivingNotSupportedErr, Agent."Agent Metadata Provider"); + if Agent.State <> Agent.State::Disabled then Error(DeactivateBeforeArchivingErr); @@ -74,6 +77,27 @@ codeunit 4301 "Agent Impl." Agent.Modify(true); end; + procedure IsArchivingSupported(AgentUserSecurityID: Guid): Boolean + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + exit(IsArchivingSupported(Agent)); + end; + + local procedure IsArchivingSupported(Agent: Record Agent): Boolean + var + AgentArchiving: Interface IAgentArchiving; + begin + if IsNullGuid(Agent."User Security ID") then + exit(false); + + AgentArchiving := Agent."Agent Metadata Provider"; + + exit(AgentArchiving.IsArchivingSupported()); + end; + procedure IsArchived(AgentUserSecurityID: Guid): Boolean var Agent: Record Agent; @@ -644,6 +668,7 @@ codeunit 4301 "Agent Impl." AgentDoesNotExistErr: Label 'Agent does not exist.'; AgentArchivedCannotBeModifiedErr: Label 'The agent is archived and cannot be modified.'; DeactivateBeforeArchivingErr: Label 'Deactivate the agent before archiving it.'; + ArchivingNotSupportedErr: Label 'Archiving agents of type ''%1'' is not supported.', Comment = '%1 = the type of the agent.'; AutoLbl: Label 'Auto'; NoActiveAgentsErr: Label 'There are no active agents setup on the system.'; NoAgentsAvailableNotificationLbl: Label 'Business Central agents are currently not available in your country.'; diff --git a/src/System Application/App/Agent/Setup/AgentList.Page.al b/src/System Application/App/Agent/Setup/AgentList.Page.al index 4237cb0de76..0f825e1a801 100644 --- a/src/System Application/App/Agent/Setup/AgentList.Page.al +++ b/src/System Application/App/Agent/Setup/AgentList.Page.al @@ -99,6 +99,9 @@ page 4316 "Agent List" if Rec.IsEmpty() then Error(NoAgentSetupErr); + if not Agent.IsArchivingSupported(Rec."User Security ID") then + Error(ArchivingNotSupportedErr, Rec."Agent Metadata Provider"); + if Agent.IsActive(Rec."User Security ID") then Error(DeactivateBeforeArchivingErr); @@ -302,4 +305,5 @@ page 4316 "Agent List" AgentIsArchived: Boolean; NoAgentSetupErr: Label 'No agents have been setup. You must set up an agent first.'; DeactivateBeforeArchivingErr: Label 'Deactivate the agent before archiving it.'; -} \ No newline at end of file + ArchivingNotSupportedErr: Label 'Archiving agents of type ''%1'' is not supported.', Comment = '%1 = the type of the agent.'; +} diff --git a/src/System Application/Test/Agent/src/SDK/AgentTest.Codeunit.al b/src/System Application/Test/Agent/src/SDK/AgentTest.Codeunit.al index 08d454abd54..a28b01df367 100644 --- a/src/System Application/Test/Agent/src/SDK/AgentTest.Codeunit.al +++ b/src/System Application/Test/Agent/src/SDK/AgentTest.Codeunit.al @@ -23,6 +23,7 @@ codeunit 133961 "Agent Test" LibraryVariableStorage: Codeunit "Library - Variable Storage"; DeactivateBeforeArchivingErr: Label 'Deactivate the agent before archiving it.', Locked = true; AgentArchivedCannotBeModifiedErr: Label 'The agent is archived and cannot be modified.', Locked = true; + ArchivingNotSupportedErr: Label 'Archiving agents of type ''%1'' is not supported.', Locked = true; local procedure Initialize() begin @@ -993,6 +994,7 @@ codeunit 133961 "Agent Test" Agent.Deactivate(AgentId); Assert.IsFalse(Agent.IsArchived(AgentId), 'Agent should not be archived initially'); + Assert.IsTrue(Agent.IsArchivingSupported(AgentId), 'Archiving should be supported for a type that does not opt out'); // [WHEN] Archiving the agent Agent.Archive(AgentId); @@ -1005,6 +1007,37 @@ codeunit 133961 "Agent Test" Assert.AreEqual(AgentRecord.Substate::Archived, AgentRecord.Substate, 'Agent substate should be Archived'); end; + [Test] + procedure ArchiveAgentOfUnsupportedTypeErrors() + var + AgentRecord: Record Agent; + TempAgentAccessControl: Record "Agent Access Control" temporary; + Any: Codeunit Any; + AgentId: Guid; + AgentUserName: Code[50]; + begin + Initialize(); + + // [SCENARIO] Archiving an agent whose type opts out of archiving is rejected + + // [GIVEN] An agent of a type that reports archiving as unsupported + AgentUserName := CopyStr(Any.AlphanumericText(MaxStrLen(AgentRecord."User Name")), 1, MaxStrLen(AgentRecord."User Name")); + AgentId := Agent.Create( + "Agent Metadata Provider"::"SDK Mock Agent No Archiving", + AgentUserName, + CopyStr(Any.AlphanumericText(80), 1, 80), + TempAgentAccessControl); + + // [THEN] Archiving is reported as unsupported for that agent + Assert.IsFalse(Agent.IsArchivingSupported(AgentId), 'Archiving should not be supported for this agent type'); + + // [WHEN] Archiving the agent + // [THEN] An error is raised and the agent is not archived + asserterror Agent.Archive(AgentId); + Assert.ExpectedError(StrSubstNo(ArchivingNotSupportedErr, "Agent Metadata Provider"::"SDK Mock Agent No Archiving")); + Assert.IsFalse(Agent.IsArchived(AgentId), 'Agent should not be archived'); + end; + [Test] procedure ArchiveActiveAgentErrors() var diff --git a/src/System Application/Test/Agent/src/Setup/LibraryMockAgent.codeunit.al b/src/System Application/Test/Agent/src/Setup/LibraryMockAgent.codeunit.al index 933e0f04381..a35be59b12f 100644 --- a/src/System Application/Test/Agent/src/Setup/LibraryMockAgent.codeunit.al +++ b/src/System Application/Test/Agent/src/Setup/LibraryMockAgent.codeunit.al @@ -49,7 +49,7 @@ codeunit 133954 "Library Mock Agent" AgentRecord: Record Agent; MockAgentSetup: Record "Mock Agent Setup"; begin - AgentRecord.SetRange("Agent Metadata Provider", AgentRecord."Agent Metadata Provider"::"SDK Mock Agent"); + AgentRecord.SetFilter("Agent Metadata Provider", '%1|%2', AgentRecord."Agent Metadata Provider"::"SDK Mock Agent", AgentRecord."Agent Metadata Provider"::"SDK Mock Agent No Archiving"); if AgentRecord.FindSet() then repeat if MockAgentSetup.Get(AgentRecord."User Security ID") then diff --git a/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.Codeunit.al b/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.Codeunit.al index fba61972d23..fd5c5b19fef 100644 --- a/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.Codeunit.al +++ b/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.Codeunit.al @@ -10,7 +10,7 @@ using System.AI; using System.Reflection; using System.Security.AccessControl; -codeunit 133952 "Mock Agent Meta. Prov." implements IAgentMetadata, IAgentFactory +codeunit 133952 "Mock Agent Meta. Prov." implements IAgentMetadata, IAgentFactory, IAgentArchiving { InherentEntitlements = X; InherentPermissions = X; @@ -21,6 +21,11 @@ codeunit 133952 "Mock Agent Meta. Prov." implements IAgentMetadata, IAgentFactor MockAgentSetup: Codeunit "Mock Agent Setup"; MockAgentInitialLbl: Label 'MA', MaxLength = 4; + procedure IsArchivingSupported(): Boolean + begin + exit(false); + end; + procedure GetDefaultInitials(): Text[4] begin exit(MockAgentInitialLbl); diff --git a/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.EnumExt.al b/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.EnumExt.al index 4a309e58cf5..75888285417 100644 --- a/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.EnumExt.al +++ b/src/System Application/Test/Agent/src/Setup/MockAgent/Integration/MockAgentMetaProv.EnumExt.al @@ -13,4 +13,10 @@ enumextension 133952 "Mock Agent Meta. Prov." extends "Agent Metadata Provider" Caption = 'SDK Mock Agent'; Implementation = IAgentFactory = "Mock Agent Meta. Prov.", IAgentMetadata = "Mock Agent Meta. Prov."; } + + value(133955; "SDK Mock Agent No Archiving") + { + Caption = 'SDK Mock Agent No Archiving'; + Implementation = IAgentFactory = "Mock Agent Meta. Prov.", IAgentMetadata = "Mock Agent Meta. Prov.", IAgentArchiving = "Mock Agent Meta. Prov."; + } }