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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions manifest/addendum.json

This file was deleted.

55 changes: 0 additions & 55 deletions manifest/manifest.json

This file was deleted.

106 changes: 106 additions & 0 deletions src/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,112 @@ a365 develop-mcp list-servers -e "myenv" --verbose
- Defaults to "prod" when no config file is specified
- Follows KISS principles to avoid over-engineering common scenarios

### Publish Command

The `publish` command packages and publishes your agent manifest to the MOS (Microsoft Online Services) Titles service. It uses **embedded templates** for complete portability - no external file dependencies required.

**Key Features:**
- **Embedded Templates**: Manifest templates (JSON + PNG) are embedded in the CLI binary
- **Fully Portable**: No external file dependencies - works from any directory
- **Automatic ID Updates**: Updates both `manifest.json` and `agenticUserTemplateManifest.json` with agent blueprint ID
- **Interactive Customization**: Prompts for manifest customization before upload
- **Graceful Degradation**: Falls back to manual upload if permissions are insufficient
- **Graph API Integration**: Configures federated identity credentials and role assignments

**Command Options:**
- `a365 publish` — Publish agent manifest with embedded templates
- `a365 publish --dry-run` — Preview changes without uploading
- `a365 publish --skip-graph` — Skip Graph API operations (federated identity, role assignments)
- `a365 publish --mos-env <env>` — Target specific MOS environment (default: prod)
- `a365 publish --mos-token <token>` — Override MOS authentication token

**Manifest Structure:**

The publish command works with two manifest files:

1. **`manifest.json`** - Teams app manifest with agent metadata
- Updated fields: `id`, `name.short`, `name.full`, `bots[0].botId`

2. **`agenticUserTemplateManifest.json`** - Agent identity blueprint configuration
- Updated fields: `agentIdentityBlueprintId` (replaces old `webApplicationInfo.id`)

**Workflow:**

```bash
# 1. Ensure you have a valid configuration
a365 config display

# 2. Run setup to create agent blueprint (if not already done)
a365 setup

# 3. Publish the manifest
a365 publish
```

**Interactive Customization Prompt:**

Before uploading, you'll be prompted to customize:
- **Version**: Must increment for republishing (e.g., 1.0.0 → 1.0.1)
- **Agent Name**: Short (≤30 chars) and full display names
- **Descriptions**: Short (1-2 sentences) and full capabilities
- **Developer Info**: Name, website URL, privacy URL
- **Icons**: Custom branding (color.png, outline.png)

**Manual Upload Fallback:**

If you receive an authorization error (401/403), the CLI will:
1. Create the manifest package locally in a temporary directory
2. Display the package location
3. Provide instructions for manual upload to MOS Titles portal
4. Reference documentation for detailed steps

**Example:**

```bash
# Standard publish
a365 publish

# Dry run to preview changes
a365 publish --dry-run

# Skip Graph API operations
a365 publish --skip-graph

# Use custom MOS environment
$env:MOS_TITLES_URL = "https://titles.dev.mos.microsoft.com"
a365 publish
```

**Manual Upload Instructions:**

If automated upload fails due to insufficient privileges:

1. Locate the generated `manifest.zip` file (path shown in error message)
2. Navigate to MOS Titles portal: `https://titles.prod.mos.microsoft.com`
3. Go to Packages section
4. Upload the manifest.zip file
5. Follow the portal workflow to complete publishing

For detailed MOS upload instructions, see the [MOS Titles Documentation](https://aka.ms/mos-titles-docs).

**Architecture Details:**

- **ManifestTemplateService**: Handles embedded resource extraction and manifest customization
- **Embedded Resources**: 4 files embedded at build time:
- `manifest.json` - Base Teams app manifest
- `agenticUserTemplateManifest.json` - Agent identity blueprint manifest
- `color.png` - Color icon (192x192)
- `outline.png` - Outline icon (32x32)
- **Temporary Working Directory**: Templates extracted to temp directory, customized, then zipped
- **Automatic Cleanup**: Temp directory removed after successful publish

**Error Handling:**

- **401 Unauthorized / 403 Forbidden**: Graceful fallback with manual upload instructions
- **Missing Blueprint ID**: Clear error message directing user to run `a365 setup`
- **Invalid Manifest**: JSON validation errors with specific field information
- **Network Errors**: Detailed HTTP status codes and response bodies for troubleshooting

## Inheritable Permissions: Best Practice

Agent 365 CLI and the Agent 365 platform are designed to use inheritable permissions on agent blueprints. This means:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ private static string GetProjectDirectory(Agent365Config config, ILogger logger)
public static Command CreateCommand(
ILogger<PublishCommand> logger,
IConfigService configService,
GraphApiService graphApiService)
GraphApiService graphApiService,
ManifestTemplateService manifestTemplateService)
{
var command = new Command("publish", "Update manifest.json IDs and publish package; configure federated identity and app role assignments");

Expand Down Expand Up @@ -116,8 +117,24 @@ public static Command CreateCommand(

// Use deploymentProjectPath from config for portability
var baseDir = GetProjectDirectory(config, logger);
var manifestPath = Path.Combine(baseDir, "manifest", "manifest.json");
var manifestDir = Path.GetDirectoryName(manifestPath)!;
var manifestDir = Path.Combine(baseDir, "manifest");
var manifestPath = Path.Combine(manifestDir, "manifest.json");

// If manifest directory doesn't exist, extract templates from embedded resources
if (!Directory.Exists(manifestDir))
{
logger.LogInformation("Manifest directory not found. Extracting templates from embedded resources...");
Directory.CreateDirectory(manifestDir);

if (!manifestTemplateService.ExtractTemplates(manifestDir))
{
logger.LogError("Failed to extract manifest templates from embedded resources");
return;
}

logger.LogInformation("Successfully extracted manifest templates to {ManifestDir}", manifestDir);
logger.LogInformation("Please customize the manifest files before publishing");
}

if (!File.Exists(manifestPath))
{
Expand Down Expand Up @@ -294,7 +311,29 @@ public static Command CreateCommand(
var fileContent = new StreamContent(zipFs);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
form.Add(fileContent, "package", Path.GetFileName(zipPath));
var uploadResp = await http.PostAsync(packagesUrl, form);

HttpResponseMessage uploadResp;
try
{
uploadResp = await http.PostAsync(packagesUrl, form);
}
catch (HttpRequestException ex)
{
logger.LogError("Network error during package upload: {Message}", ex.Message);
logger.LogInformation("The manifest package is available at: {ZipPath}", zipPath);
logger.LogInformation("You can manually upload it at: {Url}", packagesUrl);
logger.LogInformation("When network connectivity is restored, you can retry the publish command.");
return;
}
catch (TaskCanceledException ex)
{
logger.LogError("Upload request timed out: {Message}", ex.Message);
logger.LogInformation("The manifest package is available at: {ZipPath}", zipPath);
logger.LogInformation("You can manually upload it at: {Url}", packagesUrl);
logger.LogInformation("When network connectivity is restored, you can retry the publish command.");
return;
}

var uploadBody = await uploadResp.Content.ReadAsStringAsync();
logger.LogInformation("Titles upload HTTP {StatusCode}. Raw body length={Length} bytes", (int)uploadResp.StatusCode, uploadBody?.Length ?? 0);
if (!uploadResp.IsSuccessStatusCode)
Expand Down Expand Up @@ -352,7 +391,28 @@ public static Command CreateCommand(
// POST titles with operationId - using tenant-specific URL
var titlesUrl = $"{mosTitlesBaseUrl}/admin/v1/tenants/packages/titles";
var titlePayload = JsonSerializer.Serialize(new { operationId });
var titlesResp = await http.PostAsync(titlesUrl, new StringContent(titlePayload, System.Text.Encoding.UTF8, "application/json"));

HttpResponseMessage titlesResp;
try
{
using (var content = new StringContent(titlePayload, System.Text.Encoding.UTF8, "application/json"))
{
titlesResp = await http.PostAsync(titlesUrl, content);
}
}
catch (HttpRequestException ex)
{
logger.LogError("Network error during title creation: {Message}", ex.Message);
logger.LogInformation("Package was uploaded successfully (operationId={Op}), but title creation failed.", operationId);
return;
}
catch (TaskCanceledException ex)
{
logger.LogError("Title creation request timed out: {Message}", ex.Message);
logger.LogInformation("Package was uploaded successfully (operationId={Op}), but title creation failed.", operationId);
return;
}

var titlesBody = await titlesResp.Content.ReadAsStringAsync();
if (!titlesResp.IsSuccessStatusCode)
{
Expand All @@ -375,7 +435,26 @@ public static Command CreateCommand(
Entities = Array.Empty<object>()
}
});
var allowResp = await http.PostAsync(allowUrl, new StringContent(allowedPayload, System.Text.Encoding.UTF8, "application/json"));

HttpResponseMessage allowResp;
try
{
using var content = new StringContent(allowedPayload, System.Text.Encoding.UTF8, "application/json");
allowResp = await http.PostAsync(allowUrl, content);
}
catch (HttpRequestException ex)
{
logger.LogError("Network error during access configuration: {Message}", ex.Message);
logger.LogInformation("Title was created (titleId={Title}), but access configuration failed.", titleId);
return;
}
catch (TaskCanceledException ex)
{
logger.LogError("Access configuration request timed out: {Message}", ex.Message);
logger.LogInformation("Title was created (titleId={Title}), but access configuration failed.", titleId);
return;
}

var allowBody = await allowResp.Content.ReadAsStringAsync();
if (!allowResp.IsSuccessStatusCode)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,12 @@
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<!-- Embed manifest templates as resources -->
<EmbeddedResource Include="Templates\manifest.json" />
<EmbeddedResource Include="Templates\agenticUserTemplateManifest.json" />
<EmbeddedResource Include="Templates\color.png" />
<EmbeddedResource Include="Templates\outline.png" />
</ItemGroup>

</Project>
4 changes: 3 additions & 1 deletion src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ static async Task<int> Main(string[] args)
var configLoggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var configLogger = configLoggerFactory.CreateLogger("ConfigCommand");
var wizardService = serviceProvider.GetRequiredService<IConfigurationWizardService>();
var manifestTemplateService = serviceProvider.GetRequiredService<ManifestTemplateService>();
rootCommand.AddCommand(ConfigCommand.CreateCommand(configLogger, wizardService: wizardService));
rootCommand.AddCommand(QueryEntraCommand.CreateCommand(queryEntraLogger, configService, executor, graphApiService));
rootCommand.AddCommand(CleanupCommand.CreateCommand(cleanupLogger, configService, executor));
rootCommand.AddCommand(PublishCommand.CreateCommand(publishLogger, configService, graphApiService));
rootCommand.AddCommand(PublishCommand.CreateCommand(publishLogger, configService, graphApiService, manifestTemplateService));

// Invoke
return await rootCommand.InvokeAsync(args);
Expand Down Expand Up @@ -217,6 +218,7 @@ private static void ConfigureServices(IServiceCollection services)
services.AddSingleton<BotConfigurator>();
services.AddSingleton<GraphApiService>();
services.AddSingleton<DelegatedConsentService>(); // For AgentApplication.Create permission
services.AddSingleton<ManifestTemplateService>(); // For publish command template extraction

// Register AzureWebAppCreator for SDK-based web app creation
services.AddSingleton<AzureWebAppCreator>();
Expand Down
Loading
Loading