Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Development

Last updated:

Run from source, understand the architecture, and extend AGW.

This chapter is for developers changing AGW or integrating with its APIs. For installation and everyday use, start with Getting started.

Complete Development setup, then use Architecture to locate the right module. Read APIs and execution protocols for client integrations and Extensions for new capabilities. Before submitting changes, follow the relevant testing and contribution checks.

1 - Development setup

Last updated:

Install dependencies and run the backend, Web, Desktop, or Mobile independently.

Prerequisites: .NET 10 SDK, Node.js 24, pnpm 12.5.1 (pinned by packageManager in src/clients/package.json), and Git. Docker Buildx is needed only for container images. These application commands run in the AGW repository; the documentation site does not depend on this toolchain.

Backend

git clone https://github.com/zxyao145/agw.git
cd agw
git config core.hooksPath .githooks
dotnet restore Agw.slnx
dotnet tool restore
dotnet run --project src/server/Agw.Standalone.Host

Initialize at http://localhost:30816/setup. Replace dotnet run with dotnet watch for hot reload.

Clients

Open another terminal and enter the workspace from the repository root:

cd src/clients
pnpm install
pnpm dev:web

Open http://localhost:3001. Desktop uses pnpm dev:desktop; its independent renderer runs on 3000 without Web. Mobile uses pnpm dev:mobile, or pnpm android:mobile / pnpm ios:mobile. Expo CNG generates native projects.

What to expect after startup

Keep the backend terminal running, then start the client you need. Once Web opens, confirm Server initialization and login, then configure a model using Your first conversation. A loaded frontend alone does not verify its backend connection.

The backend defaults to 30816, Web development to 3001, and the Desktop renderer to 3000. If a port is occupied, check for an existing development process. On a physical phone, localhost means the phone itself; use a computer address reachable from the phone.

Verify

Confirm backend initialization, client connectivity, and a simple message. Physical mobile devices need a reachable backend address. External CLIs must work in the execution process environment.

The site needs Hugo Extended and Go, plus Python 3 for the scripts/check-site.py verifier; see site/README.md for commands. Do not add it to the client Turborepo or make Web/Desktop consume its artifacts.

Implementation and references

2 - Architecture and module boundaries

Last updated:

Understand the modular monolith, data ownership, and client package responsibilities.

Prerequisite: a working source setup. Identify the business owner of a use case before tracing cross-module capabilities through Contracts.

Backend organization

AGW is a modular monolith. Agw.Host supplies shared hosting; Control Plane, Data Plane, and Standalone compose the modules they need. Business modules follow Api → Application → Domain ← Infrastructure, creating only necessary layers.

flowchart LR
    API[Api] --> APP[Application]
    APP --> DOMAIN[Domain]
    INFRA[Infrastructure] --> DOMAIN
LayerResponsibilityWhat to inspect
ApiReceive requests and return responsesRoutes, inputs, and outputs
ApplicationComplete a business operationAuthorization, queries, transactions, and call order
DomainHold business data and express rulesEntities, Behaviors, and DomainServices
InfrastructureConnect databases and external systemsPersistence and concrete adapters

Domain entities hold state. A Behavior handles rules within one Aggregate, while a DomainService handles rules that need facts beyond it. Application loads data, coordinates these calls, and persists changes. Ordinary CRUD stays in Application without creating a Behavior for every entity.

Data ownership

Each table has one owning module. Sharing entity types and a database does not permit direct access to another module’s tables; use that module’s published interfaces.

Each module that owns tables declares its persistence interface I<Module>DbContext in Application/Persistence. There are nine: Agents, Auth, Integrations, Jobs, Projects, Providers, Settings, Skills, and Tools. Files, Setup, and A2A own no tables and have no such interface. Within a request, one AgwDbContext instance implements these interfaces. Modules share database resources while limiting the data each can access. Cross-module calls use Contracts; approved Infrastructure adapters handle cross-module transactions.

Agw.Agents.Execution → Agw.Agents is one-way; both assemblies belong to the Agents module. Selective DDD for Agentflows does not extend to ordinary CRUD modules.

Example: updating an Agentflow

Api receives the request. Application checks access and loads the flow with all nodes and edges. Policy validates the proposed graph and returns a Decision. Behavior applies valid changes to the loaded objects, then Application saves them.

For edge rules, inspect the Agentflow Policy and Topology. For authorization or loading and saving order, inspect Application. For database implementation, inspect Infrastructure. This separates business rules from network and storage details and makes them easier to test independently.

Clients

Web and Desktop own independent route shells and builds. Business packages live in src/clients/packages. chat-core owns message semantics, chat-runtime owns execution connections and state, and chat owns DOM rendering. Mobile uses chat-native and RN-safe packages rather than DOM packages.

Identify the owning module and public entry point before adding a feature. Run pnpm test:boundaries and the backend architecture tests, dotnet test tests/Agw.Architecture.Tests, after boundary changes.

Implementation and references

3 - APIs and execution protocols

Last updated:

Distinguish management JSON APIs, SignalR execution, and A2A.

Prerequisites: access to a development Server and a valid authenticated identity, such as an API Key or a browser session. Use current OpenAPI and owning-module Contracts for exact fields; this site does not duplicate the complete schema.

Protocol boundaries

InterfacePurpose and contract
Management JSON APIsBens.Results ApiResult envelopes, unwrapped by typed client helpers
/api/hubs/execSignalR execution commands, state, and events; mapped by Data Plane and Standalone only and accepts only the WebSocket transport. The official client connects with skipNegotiation: true
/api/agents/permission-capabilitiesQuery supported permissions for a target
/api/auth/oidc/providersEnabled sign-in providers, used to render the sign-in buttons
/api/auth/oidc/loginRedirects to the provider; client is web or desktop
/api/auth/desktop/exchangeDesktop exchanges a one-time code plus its verifier for an API Key
A2AProtocol-specific responses, mapped by Data Plane and Standalone only
/openapi/*Contract entry point; served, together with the Scalar API reference, only in the Development environment by Control Plane or Standalone

Sign-in routes are served by Control Plane and Standalone. An API Key obtained by Desktop behaves like a manually created one and accesses resources as its creator.

Integration sequence

  1. Use an API Key in the Authorization: Bearer header for automation. Browser requests using Cookies to make changes, such as POST, PUT, and DELETE, also need the existing CSRF protection flow to prevent another site from acting through the signed-in session.
  2. Read resources accessible to the current user through management APIs and retain their stable identifiers.
  3. Query target permission capabilities, then use the existing execution protocol to send commands and subscribe to events.
  4. Restore conversation/execution state after reconnecting; disconnection is not completion.

New endpoints default to query/body identifiers under repository rules. Consult current OpenAPI and Contracts for each endpoint’s routes and parameters.

Handle results in the client

Management JSON APIs use Bens.Results response envelopes. Reuse the typed helpers in @agw/api to extract business data, and handle request failures and application errors separately.

An execution connection returns a stream of events. Retain conversation and execution IDs, show tool activity and input requests, and recover actual progress after reconnecting. Partial text is not completion, and disconnection is not cancellation. See the execution protocol for message shapes and order.

Agent structured-response fields

responseSchema holds the JSON Schema text configured on an agent. It appears in the full agent response: GET /api/agents/{id}, GET /api/agents/paged, POST /api/agents, PUT /api/agents/{id}, and PUT /api/agents/enabled. The selector endpoint GET /api/agents omits it.

Both response shapes include resultFormat, either markdown or json, derived from whether a schema is configured. Clients use it to decide how to render the final result without parsing the schema themselves.

On update, an absent field keeps the current value, null or a whitespace-only string clears it, and any other string replaces it. The server requires valid JSON whose root is an object and otherwise returns an invalid-parameter error. Pi agents reject any schema with an invalid-parameter error. The schema is stored and forwarded as text only.

Contract changes

Keep DTOs in the owning module’s Contracts. Expected application failures use AgwException and stable seven-digit ErrorCodes, mapped at boundaries. WebSocket, OAuth redirects, A2A, and static files retain their protocols.

After backend contract changes, export the Development OpenAPI document to src/clients/packages/api/openapi.json, run pnpm gen:api from src/clients, and validate callers. gen:api converts only that local file; it does not fetch the latest document from Server. Do not hand-edit generated openapi.d.ts or log actual API Keys.

Implementation and references

4 - Extend tools and integrations

Last updated:

Choose capability ownership and use generated tools and user connections.

Prerequisites: understand module boundaries and decide whether the capability is general-purpose or business-owned. Begin with one small, verifiable capability.

Tool extension path

  1. Put hand-written IAgwTool, IContextualTool, and IToolBlock implementations in Agw.Tools; the global catalog scans only that assembly for hand-written tools. Business tools belong in their module’s Application/Tools, declared as attributed containers or supplied through a Skill, with DTOs in Contracts/Tools.
  2. Reference Agw.Tools.Abstractions; add Agw.Tools.Generators as an Analyzer for attributed declarations.
  3. Explicitly declare permissions, argument descriptions, and return types. Standalone tools and attributed containers stay stateless; session state belongs in a Provider, session, or owned storage.
  4. Register services and generated declarations in the owning module. Choose Skill exposure or explicit global catalog inclusion.
  5. Verify discovery, arguments, permissions, project binding, and error mapping.

The generator emits metadata, JSON Schema, and direct invocation delegates. Do not add runtime reflection scanning as a fallback. Skill tools come from two members: IAgentSkillRegistration.Tools supplies hand-written IProjectScopedAgwTool instances, and ToolTypes supplies attributed container types generated through IAgwToolSet<T>. They bind to a Project during execution. Registering a generated module does not automatically expose every tool globally.

Integration extension path

IPluginCatalog owns Plugin, Connector, authentication, and capability-source definitions. Definitions are code/content assets. User setup is PluginInstallation; selectable accounts or endpoints are Connection. Do not collapse these into global configuration.

Add the catalog definition and required capability source, then verify per-user setup, Ready state, binding, and invocation. Infrastructure protects and resolves credentials. Reads and execution retain owner checks. Credential injection over HTTP/SSE requires HTTPS.

Choose global or Skill-owned tools

Expose independently selectable general-purpose operations explicitly in the global catalog. Register tools used only by a Skill through that Skill, so instructions and tools reach the agent together. For example, agw-job supplies job-management tools owned by the Jobs module.

After compilation, verify that the agent can discover the tool. If it is missing, check generated declarations and registration. If invocation fails, check Project binding, permissions, and arguments. Compilation alone does not verify runtime integration.

Option 1: define a Tool with an interface

Use IAgwTool for one independently callable operation per class. Declare a stable name, category, Plan availability, and permission, and implement the one required member, ToAITool(). Repository tools put the operation in an Execute method and wrap it in ToAITool() with AgwAIFunctionFactory.CreateParameterObjectFunction. That factory is internal to Agw.Tools, so the example lives in Agw.Tools. This minimal example performs no external I/O:

using System.ComponentModel;
using Agw.Tools.Abstractions;
using Agw.Tools.Infrastructure;
using Microsoft.Extensions.AI;

public sealed class EchoInput
{
    [Description("Text to return unchanged.")]
    public string Text { get; set; } = "";
}

public sealed class EchoTool : IAgwTool
{
    public string Name => "echo";
    public string Category => "Examples";
    public bool AllowInPlanMode => true;
    public AgwToolPermission RequiredPermission => AgwToolPermission.None;

    [Description("Return the supplied text unchanged.")]
    public string Execute(EchoInput input) => input.Text;

    public AITool ToAITool()
    {
        Func<EchoInput, string> func = Execute;
        return AgwAIFunctionFactory.CreateParameterObjectFunction(func, Name);
    }
}

IAgwTool extends the metadata interface IAgwToolMeta; ToAITool() wraps the execution method as a model-callable function. Describe the method and input DTO so the model knows when and how to call it. Use asynchronous methods and propagate CancellationToken for real I/O. Business DTOs belong in the owner module’s Contracts/Tools.

Register and use it

  1. Put the implementation in Agw.Tools/Impl/Tools. The global catalog scans only the Agw.Tools assembly for hand-written tools, so an IAgwTool placed in a business module is never discovered. Business modules use Option 2’s attributed containers instead, or place IProjectScopedAgwTool instances in a Skill’s IAgentSkillRegistration.Tools. Keep declarations stateless; do not store the current user, Project, or conversation in fields.
  2. In src/server/Agw.Shared/Tooling/ToolValueObject.cs, add the tool name to ToolDefinitionNames and its All list, plus a concrete ToolDefinition, a [JsonDerivedType] name mapping, and an empty or real Options type. Definitions and implementations must match one to one. An unregistered name fails registration with “does not have a registered ToolDefinition”.
  3. Register dependencies through the owner module’s DI entry point. Verify /api/tools and bind the tool to an Agent or Project before using it.
  4. Ask the agent to invoke it and inspect arguments and output. Direct C# calls test the operation but do not verify runtime permission or Project binding.

For standalone tools requiring a Project or runtime directory, use IContextualTool.MaterializeAsync (defined in Agw.Tools/Contracts/Abstractions and, like other hand-written tools, discovered only in the Agw.Tools assembly) and bind authorized context into contributed functions. Do not let a model-supplied Project ID determine resource ownership.

Option 2: define Tools with attributes

Attributes work well for multiple operations in a service or business capabilities supplied by a Skill. Add these project references, adjusting relative paths:

<ItemGroup>
  <ProjectReference Include="../Agw.Tools.Abstractions/Agw.Tools.Abstractions.csproj" />
  <ProjectReference Include="../Agw.Tools.Generators/Agw.Tools.Generators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

This implements the same echo operation as the interface example. Choose one implementation; registering both creates a name collision.

using System.ComponentModel;
using Agw.Tools.Abstractions;
using Agw.Tools.Abstractions.Attributes;

[AgwToolContainer(AgwToolPermission.None,
    DefaultCategory = "Examples", AllowInPlanMode = true)]
public sealed class TextTools
{
    [AgwTool("echo", AgwToolPermission.None)]
    [Description("Return the supplied text unchanged.")]
    public static string Echo([Description("Text to return.")] string text)
    {
        return text;
    }

    [AgwToolIgnore]
    public static string FormatForDisplay(string text) => text.Trim();
}

The example container is an ordinary sealed class whose methods are all static. IAgwToolSet<TextTools>, used later, requires a non-static class as its type argument, so the container cannot be a static class. AgwToolContainer selects public ordinary methods declared directly on the type. AgwTool supplies a name and permission; AgwToolIgnore excludes helpers. Default names use the method name minus a terminal Async. Permissions must be explicitly declared or inherited from the container; AllowInPlanMode is independent.

Instance containers use explicit constructor injection and must be registered in DI. Static methods can use parameters marked AgwToolService. Service and CancellationToken parameters are excluded from model-facing schemas. Each invocation gets an independent asynchronous DI scope.

Generate, register, and select

Compilation generates metadata, input/output schemas, and direct invocation delegates. For an assembly named My.Module, register its generated module, then explicitly select container types if they belong in the global catalog:

// Example assembly name: My.Module
services.AddSingleton<IAgwGeneratedToolModule>(
    Agw.Generated.My.Module.AgwToolModule.Instance);
// Only for tools intended for the global catalog:
services.AddToolCatalogTypes(typeof(TextTools));

Import the generated contracts and relevant registration extensions. Containers with only static methods need no instance registration; containers with instance methods also need AddScoped(). A generated module registers declarations without exposing every tool globally. Global tools still require concrete ToolDefinition types and JSON mappings.

For Skill-only tools, make the registration partial and implement IAgwToolSet so the generator supplies ToolTypes. Complete the remaining IAgentSkillRegistration members, including identity, description, and creation logic. Register the Skill in the owner module’s DI entry point with services.AddSingleton<IAgentSkillRegistration, YourSkillRegistration>(), along with its generated module and instance containers; see JobManagementSkillRegistration in the Jobs module. Bind the Skill to an Agent/Project to contribute its tools. Hand-written Skill tools implement IProjectScopedAgwTool and go in IAgentSkillRegistration.Tools; they do not need global catalog entries.

Fix generator diagnostics rather than adding reflection fallbacks: AGWTOOL001 identifies unsupported signatures; AGWTOOL002 identifies invalid declarations. See the tool abstraction guide for complete instance-container and Skill examples.

ToolBlocks: tools with shared state

Use a ToolBlock when operations such as adding, completing, and listing todos must maintain one coherent state. Members are selected as a group. An attribute container is a declaration mechanism, not session-state isolation.

Here is the complete repository TodoToolBlock:

using Agw.Tools.ToolBlocks;

namespace Agw.Tools.Impl.ToolBlocks.Todo;

public sealed class TodoToolBlock : IToolBlock
{
    public ToolBlockDescriptor Descriptor { get; } =
        new(
            ToolBlockNames.Todo,
            "Todo",
            "Tracks multi-step work with a persistent todo list.",
            ToolBlockScope.Agent | ToolBlockScope.Project,
            [
                new("todos_add", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_complete", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_remove", AgwToolPermission.None, allowInPlanMode: true),
                new("todos_get_remaining", AgwToolPermission.ReadOnly, allowInPlanMode: true),
                new("todos_get_all", AgwToolPermission.ReadOnly, allowInPlanMode: true),
            ]
        );

    public ValueTask<ToolContribution> MaterializeAsync(
        ToolBlockDefinition definition,
        ToolMaterializationContext context,
        CancellationToken cancellationToken
    )
    {
        var contribution = new ToolContribution();
        contribution.PlanModeAllowedToolNames.UnionWith(
            Descriptor.Members.Where(static member => member.AllowInPlanMode).Select(static member => member.Name)
        );
        contribution.ContextProviders.Add(new AgwTodoProvider());
        var evaluatorOptions = context.EnabledToolBlockNames.Contains(ToolBlockNames.Mode)
            ? new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] }
            : null;
        contribution.LoopEvaluators.Add(new TodoCompletionLoopEvaluator(evaluatorOptions));
        return ValueTask.FromResult(contribution);
    }
}

Todo state and lifetime

  • Descriptor.Members declares all five members, permissions, and Plan availability. Members cannot be separately registered as global tools.
  • MaterializeAsync creates an AgwTodoProvider in ToolContribution.ContextProviders and adds a loop evaluator.
  • AgwTodoProvider keeps AgwTodoState in the current AgentSession.StateBag and declares its key through StateKeys. Operations read, modify, and save the current session’s state; a static list or singleton must not hold everyone’s todos.
  • Later calls in the same session reuse state; different sessions are isolated. Durable recovery depends on the surrounding session save/restore pipeline, not merely storing data in a Provider field.
  • TodoCompletionLoopEvaluator checks outstanding items. When Mode is enabled, it evaluates in Execute mode. Custom ToolBlocks only need an evaluator if their behavior requires one.

Implement your own stateful ToolBlock

  1. Define the data and its lifetime: turn, session, or persistent project storage. Use AgwTodoState for session state and Project Memory for project storage as references.
  2. In Agw.Shared/Tooling/ToolValueObject.cs, add the name to ToolBlockDefinitionNames and its All list, plus a concrete ToolBlockDefinition, Options, and [JsonDerivedType] mapping. Then add a runtime name in ToolBlockNames that references that constant. The startup coverage check rejects ToolBlocks missing a definition or an implementation.
  3. Implement IToolBlock and declare every member’s permission and allowInPlanMode.
  4. Create Providers in MaterializeAsync, bind functions to the current context, and transfer lifetime ownership to ToolContribution. Do not cache Providers or scoped services across users.
  5. Wire group selection into catalog and definition resolution. Test adding, completing, removing, session isolation, save/restore, Plan restrictions, and approvals.

Read the Todo Provider and Todo state; copying the descriptor alone omits state loading and saving.

Plugins: from catalog definition to invocation

A Plugin here is code and content in AGW’s integration catalog. GitHub is the built-in example. Adding a Plugin requires changing and building the server; arbitrary uploaded packages are not executed. Available integrations shows catalog definitions; Configured integrations shows user accounts or endpoints.

Developer objectResponsibility
PluginDefinitionStable ID, version, display name, connectors, optional Skill content
ConnectorDefinitionService or protocol variant, such as GitHub Cloud
AuthSchemeDefinitionAuthentication method, configuration fields, OAuth settings
CapabilitySourceDefinitionInternal C# tools or tools obtained from MCP
PluginInstallationPer-user setup, such as OAuth client ID/secret
ConnectionUser account, credentials, Ready state; bindings use ConnectionId

Define the catalog and authentication

Use this existing GitHub catalog as a complete structural reference. Replace OAuth endpoints, scopes, and fields with those required by your target service:

using Agw.Integrations.Application.Plugins;
using Agw.Integrations.Domain.Plugins;

namespace Agw.Integrations.Infrastructure.Plugins;

/// <summary>
/// 所有可以使用的 plugin 列表
/// </summary>
public sealed class BuiltInPluginCatalog : IPluginCatalog
{
    private static readonly IReadOnlyList<PluginDefinition> Plugins =
    [
        new PluginDefinition
        {
            Id = "github",
            Version = "1.0.0",
            DisplayName = "GitHub",
            Description = "Connect GitHub accounts and use repository capabilities.",
            Tags = ["Git", "Coding"],
            Connectors =
            [
                new ConnectorDefinition
                {
                    Id = "github-cloud",
                    DisplayName = "GitHub Cloud",
                    Description = "Connect a GitHub.com account using OAuth.",
                    AuthSchemes =
                    [
                        new AuthSchemeDefinition
                        {
                            Id = "oauth2",
                            DisplayName = "OAuth 2.0",
                            Type = AuthSchemeType.OAuth2,
                            OAuth2AuthorizationCode = new OAuth2AuthorizationCodeSettings
                            {
                                AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
                                TokenEndpoint = "https://github.com/login/oauth/access_token",
                                UserInfoEndpoint = "https://api.github.com/user",
                                ClientIdFieldId = "client-id",
                                ClientSecretFieldId = "client-secret",
                                SubjectResolution = new OAuthSubjectResolutionDefinition
                                {
                                    Source = OAuthSubjectSource.UserInfo,
                                    Field = "login",
                                },
                                UsePkce = true,
                                ClientAuthenticationMethod = OAuth2ClientAuthenticationMethod.Body,
                                SupportsRefresh = false,
                                Scopes = ["repo", "read:user", "read:org"],
                            },
                            InstallationFields =
                            [
                                new FormFieldDefinition
                                {
                                    Id = "client-id",
                                    Label = "Client ID",
                                    Type = FormFieldType.Text,
                                    IsRequired = true,
                                },
                                new FormFieldDefinition
                                {
                                    Id = "client-secret",
                                    Label = "Client secret",
                                    Type = FormFieldType.Secret,
                                    IsRequired = true,
                                },
                            ],
                        },
                    ],
                    CapabilitySources =
                    [
                        // Agw 内部 C# Provider 创建工具。
                        new NativeCapabilitySourceDefinition { Id = "github-native", Provider = "github" },
                    ],
                },
            ],
            Skills = [new PluginSkillDefinition { ContentPath = "Plugins/github/skills/github/SKILL.md" }],
        },
    ];

    public BuiltInPluginCatalog()
    {
        PluginCatalogValidator.Validate(Plugins);
    }

    public IReadOnlyList<PluginDefinition> List()
    {
        return Plugins;
    }

    public PluginDefinition? Find(string pluginId)
    {
        return Plugins.FirstOrDefault(plugin => string.Equals(plugin.Id, pluginId, StringComparison.OrdinalIgnoreCase));
    }
}

Keep Plugin, Connector, authentication, and source IDs stable. Validate the full catalog with PluginCatalogValidator. InstallationFields describe per-user setup; account fields belong in the authentication scheme’s connection fields. Use Secret field types and never embed real credentials in definitions.

Implement capability sources

Native: the definition’s Provider = “github” matches IConnectionNativeCapabilityProvider.Provider. Implement CreateTools(ConnectionNativeCapabilityContext), binding resolved ConnectionId, Alias, and ProjectId. Use names such as {alias}__{operation} to distinguish accounts.

Follow GitHubConnectionNativeCapabilityProvider: functions bind the account and Project, then create a scope and resolve IGitHubConnectionInvoker when invoked. The Invoker checks ownership, Ready state, and credentials. Do not accept arbitrary model-supplied ConnectionIds or cache account secrets in a singleton. Wire new operations into the source’s permission metadata and approval pipeline.

MCP: use McpCapabilitySourceDefinition with stdio, HTTP, or SSE transport. CredentialBindings map installation fields, connection fields, or OAuth tokens to environment variables or HTTP headers. Use HTTPS when sending credentials over the network and keep field references consistent with authentication definitions. This path retains connection authorization and runtime validation rather than passing unchecked URLs to agents.

Register, package content, and test

  1. Maintain catalog registration in the Integrations DI entry point. Register Native providers as IConnectionNativeCapabilityProvider and invocation services with suitable lifetimes, such as the scoped GitHub Invoker.
  2. Place optional Skill content in the Plugin content directory and point PluginSkillDefinition.ContentPath to SKILL.md. It provides instructions, not automatic execution of third-party scripts. Ensure published artifacts include these files.
  3. Find the definition in Available integrations, configure setup and an account for a test user, complete authentication, verify Ready, and bind it to an Agent or Project.
  4. Test a read and a controlled write, including tool names, arguments, permissions, and errors. Tests use real implementations, never mocks or fake implementations; tests that need real accounts or OAuth authorization stay out of the default suite.
  5. Cover foreign ConnectionIds, unready accounts, expired credentials, invalid catalog fields, duplicate tool names, and configuration changes. Updating one user’s installation settings must affect only that user’s connections.

There is currently no remote Marketplace download, signature, or automatic upgrade mechanism. Follow the GitHub Native Provider, GitHub Invoker, and capability-source definitions.

Verify

Cover successful invocation, invalid arguments, insufficient permissions, foreign Connections, and non-Ready Connections. Keep compile-time diagnostics effective. Tests use real implementations, never mocks or fake implementations; tests that depend on real accounts or external CLIs are opt-in and stay out of the default suite.

Implementation and references

5 - Testing and contribution

Last updated:

Validate the affected behavior and follow formatting, boundary, and migration rules.

Prerequisite: dependencies are installed. Read root AGENTS.md and the relevant rules under docs/human/ before changes, and preserve unrelated local work.

Backend checks

From the repository root:

dotnet build Agw.slnx
dotnet test Agw.slnx
dotnet csharpier check .

Test projects use xUnit v3 and run on Microsoft.Testing.Platform, selected by the root global.json. Start with relevant tests when investigating a failure, such as dotnet test tests/Agw.Files.Tests. Unit and composition tests use real implementations and pure option helpers, never mocks or fake implementations. Constructing CodexAIAgent or ClaudeCodeAIAgent probes the CLI, so those tests run as real CLI tests: they are opt-in, require the executable, and stay out of the default suite.

After changing error codes or exception rules, run dotnet test tests/Agw.Shared.Tests. After changing module dependencies, run the backend architecture tests with dotnet test tests/Agw.Architecture.Tests.

PostgreSQL tests for durable execution (lease protection, event order, active execution upgrades, and scheduler capacity) connect to an isolated test instance through AGW_TEST_POSTGRES_CONNECTION_STRING; the test role needs permission to create databases. Redis event-projection tests use AGW_TEST_REDIS_CONNECTION_STRING. Without these variables, the corresponding tests are skipped. CI runs the PostgreSQL tests on PostgreSQL 18 and checks the TRX results to confirm every required test ran and passed; see the Development guide for the full commands.

For sign-in changes, run dotnet test tests/Agw.Auth.Tests. These tests use a controlled provider and per-test SQLite databases by default. To verify against PostgreSQL, set AGW_TEST_OIDC_POSTGRES to an isolated test server’s admin connection string with a role that can create databases; never point it at a production server. Desktop main-process sign-in and credential-storage tests run with pnpm --filter @agw/desktop test.

Client checks

From src/clients:

pnpm lint
pnpm test
pnpm fmt:check
pnpm build

Use oxlint/oxfmt, not ESLint/Prettier. Package-boundary changes must pass pnpm test:boundaries. After API changes, export the Development OpenAPI document to src/clients/packages/api/openapi.json, run pnpm gen:api to regenerate the typed client, and validate callers.

Component rendering tests use the shared @agw/test-harness package to set up a DOM environment. When a test needs API responses, its startApiServer starts a real local HTTP server so components exercise their own request path. For Web browser tests, run pnpm --filter @agw/web exec playwright install chromium, then pnpm --filter @agw/web test:e2e. Playwright starts an isolated Web server on 127.0.0.1:3101 and needs no backend.

Data and commits

Model changes need matching SQLite and PostgreSQL migrations, but generate or apply them only with explicit authorization. Use src/server/Agw.Migrations.Sqlite or src/server/Agw.Migrations.Postgres as the migrations project and src/server/Agw.Standalone.Host as the startup project, ending the command with -- --provider sqlite or -- --provider postgres; see the Development guide for the full commands. dotnet tool restore installs only CSharpier, so install dotnet ef separately. NoForeignKeyModelDiffer prohibits database foreign keys; Application/Infrastructure own reference validation and cleanup.

Use explicit C# constructors, not primary constructors, and DateTimeOffset for dates. Follow root AGENTS.md. Commits need explicit authorization and use Conventional Commits.

Match checks to the change

ChangeMinimum verification
Backend behavior fixBuild the affected project and verify a reproduction of the original problem
API or DTO changeExport the OpenAPI document, regenerate API types, and verify callers and error handling
Module or package dependenciesRun backend architecture tests or client boundary checks
Error code changePass tests/Agw.Shared.Tests
UI changeInspect real interactions, screen widths, and relevant tests; run test:e2e for Web browser behavior
Site documentationPass strict Hugo, link, and translation checks and inspect rendered pages

Keep failure details, fix the cause, and rerun affected checks. Describe the change, validation, and any database or deployment impact when submitting it.

Completion criteria

Bug fixes need reproducible verification. Behavior changes cover relevant success and failure paths. Changes limited to site use its Hugo, link, and browser checks; documentation-only work does not need model services or database initialization.

Implementation and references