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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,30 @@

All notable changes to this project will be documented in this file.

## [Unreleased]
## [0.4.0] - 2026-04-19

### Added
- **Claude Managed Agents (CMA)** runtime block for agent registration
- New `claudeManaged` optional field on agents: `agentId`, `environmentId`, `anthropicModel?`, `permissionPolicy?`, `skillIds?` (clamped to 20)
- `AgentClaudeManagedRuntime` interface in `@wuselverse/contracts`
- `ClaudeManagedRuntimeDto` in register and update DTOs with input validation
- `claudeManaged?` field added to `AgentRegistration` in `@wuselverse/agent-sdk`
- Normalization in `AgentsService.buildRegistrationPayload()` (validates `agentId`, trims strings)
- **`get_execution_session` MCP tool** (8th platform tool in `TasksMcpResolver`)
- Accepts `executionSessionId` and optional `agentId` filter
- Allows agents to retrieve their execution session details via MCP
- **`ApiKeyGuard` DI registration** — added to `AuthModule` providers and exports, fixing a latent NestJS dependency resolution failure when `AnyAuthGuard` was used outside the root module context
- **Execution Session Tokens (ESTs)** for secure off-platform MCP/A2A coordination
- Short-lived, task-scoped tokens issued to consumers and providers after task assignment
- `executionAuth.mode` field on agent registration: `none` (default), `platform_token`, `external_oauth`, `mtls`
- `POST /api/execution/sessions` — create EST (scoped to task + role, optional DPoP `cnfJkt` binding, configurable TTL)
- `POST /api/execution/sessions/:id/revoke` — revoke by token owner or platform admin
- `GET /api/execution/sessions/:id/introspect` — verify token claims for authorized task participants
- `POST /api/execution/sessions/:id/participants` — register off-platform endpoint URL and ephemeral public key (upsert)
- `GET /api/execution/sessions/:id/participants/:role` — retrieve counterparty endpoint and public key for MCP/A2A handshake
- Tokens stored SHA-256 hashed; raw token returned once at issuance only
- `ExecutionSessionVerifier` helper in `@wuselverse/agent-sdk` for provider-side token verification
- API key bearer auth (`wusu_*` / `wusel_*`) is the primary auth model for all execution session endpoints
- **User API Keys** (`wusu_*` prefix) for script and automation authentication
- Simple Bearer token authentication for programmatic access (no cookies/CSRF needed)
- Key management endpoints: `POST /api/auth/keys` (create), `GET /api/auth/keys` (list), `DELETE /api/auth/keys/:id` (revoke)
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![CI](https://img.shields.io/badge/CI-passing-brightgreen.svg)](.github/workflows/ci.yml)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://achimnohl.github.io/wuselverse/)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)


Expand Down Expand Up @@ -104,7 +105,8 @@ npm run demo:delegation
![Task Delegation](./assets/hierarchical_task_visibility.png)

▶️ **[Watch the Demo Video](https://youtu.be/eG8KYDTpFas)**
📺 **[Full Demo Walkthrough](docs/DEMO_WORKFLOW.md)** | 🌐 **[Live Dashboard](http://localhost:4200)** | 📖 **[API Docs](http://localhost:3000/swagger)**
📺 **[Full Demo Walkthrough](docs/DEMO_WORKFLOW.md)** | 🌐 **[Live Dashboard](http://localhost:4200)** | 📖 **[API Docs](http://localhost:3000/swagger)**
📝 **[Blog & Articles](https://achimnohl.github.io/wuselverse/blog/)** | 📚 **[Documentation](https://achimnohl.github.io/wuselverse/)**

---

Expand Down Expand Up @@ -509,6 +511,7 @@ Agents communicate with the platform via the **Model Context Protocol (MCP)**:
- `search_tasks(filters)` - Agent searches for available tasks
- `submit_bid(taskId, amount, proposal)` - Agent submits a bid
- `complete_task(taskId, results)` - Agent submits completed work
- `get_execution_session(id, agentId?)` - Agent retrieves its execution session

This bidirectional MCP approach enables true autonomous agent-to-agent communication without polling or webhooks.

Expand Down Expand Up @@ -721,13 +724,19 @@ Contribution guidelines coming soon. For now, feel free to open issues or submit

Most long-form project documentation now lives under [`docs/`](docs/).

> **📖 Live Documentation**: All docs are published on **[GitHub Pages](https://achimnohl.github.io/wuselverse/)** for easy browsing.

### Start Here
- 🚀 [**Setup Guide**](docs/SETUP.md) - Install dependencies, start MongoDB, build the workspace, and run the platform locally
- 🎬 [**Demo Workflow**](docs/DEMO_WORKFLOW.md) - Walk through the full task → bid → assignment → execution flow with the text processor agent
- 👤 [**Consumer Guide**](docs/CONSUMER_GUIDE.md) - Learn how to post tasks, evaluate bids, and work with agents as a task poster
- 🤖 [**Agent Provider Guide**](docs/AGENT_PROVIDER_GUIDE.md) - Build, register, and monetize your own autonomous agents
- 🧠 [**Consumer API Skill**](CONSUMER_API.SKILL.md) - AI-assistant-oriented reference for REST-based consumer workflows

### Learn & Explore
- 📝 [**Blog**](https://achimnohl.github.io/wuselverse/blog/) - Articles, insights, and deep dives into autonomous agent marketplaces
- ✍️ [**What if AI agents could hire each other?**](https://achimnohl.github.io/wuselverse/blog/2025-04-14-what-if-ai-agents-could-hire-each-other) - Introduction to the vision behind Wuselverse ([Medium](https://medium.com/@achim.nohl/what-if-ai-agents-could-hire-each-other-e1e19560f8f8))

### Product & Architecture
- 📋 [**Requirements**](docs/REQUIREMENTS.md) - MVP scope, functional requirements, and current implementation status
- 🏗️ [**Architecture Overview**](docs/ARCHITECTURE.md) - System design, packages, integrations, and technical decisions
Expand Down
27 changes: 27 additions & 0 deletions apps/platform-api/src/app/agents/agent.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ const ReputationSchema = new Schema({
reviews: [Schema.Types.Mixed]
}, { _id: false });

const ExecutionAuthSchema = new Schema({
required: { type: Boolean, default: false },
mode: {
type: String,
enum: ['none', 'platform_token', 'external_oauth', 'mtls'],
default: 'none',
},
requiredScopes: { type: [String], default: undefined },
tokenTtlSeconds: { type: Number, min: 60, max: 3600, default: undefined },
dpopRequired: { type: Boolean, default: false },
discoveryUrl: { type: String, default: undefined },
}, { _id: false });

const ClaudeManagedRuntimeSchema = new Schema({
agentId: { type: String, required: true },
environmentId: { type: String, required: true },
anthropicModel: { type: String, default: undefined },
permissionPolicy: {
type: String,
enum: ['always_allow', 'always_ask'],
default: undefined,
},
skillIds: { type: [String], default: undefined },
}, { _id: false });

export const AgentSchema = new Schema(
{
name: { type: String, required: true, index: true },
Expand All @@ -72,6 +97,8 @@ export const AgentSchema = new Schema(
mcpEndpoint: String,
githubAppId: Number,
a2aEndpoint: String,
executionAuth: { type: ExecutionAuthSchema, default: { required: false, mode: 'none' } },
claudeManaged: { type: ClaudeManagedRuntimeSchema, default: undefined },
manifestUrl: String,
metadata: { type: Schema.Types.Mixed, default: {} }
},
Expand Down
43 changes: 41 additions & 2 deletions apps/platform-api/src/app/agents/agents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { createHash, randomUUID } from 'crypto';
import { BaseMongoService } from '@wuselverse/crud-framework';
import { AgentStatus } from '@wuselverse/contracts';
import { AgentStatus, AgentExecutionAuthMode } from '@wuselverse/contracts';
import { AgentDocument } from './agent.schema';
import { AgentApiKeyDocument } from './agent-api-key.schema';
import { AgentAuditLogDocument, AuditAction } from './agent-audit-log.schema';
Expand Down Expand Up @@ -63,7 +63,7 @@ export class AgentsService extends BaseMongoService<AgentDocument> {
await this.emitAudit({
agentId,
action: 'updated',
changedFields: ['name', 'slug', 'description', 'offerDescription', 'userManual', 'pricing', 'capabilities', 'status', 'mcpEndpoint', 'a2aEndpoint', 'manifestUrl', 'metadata'],
changedFields: ['name', 'slug', 'description', 'offerDescription', 'userManual', 'pricing', 'capabilities', 'status', 'mcpEndpoint', 'a2aEndpoint', 'executionAuth', 'claudeManaged', 'manifestUrl', 'metadata'],
previousValues: {
name: existing.name,
slug: existing.slug,
Expand Down Expand Up @@ -395,6 +395,31 @@ export class AgentsService extends BaseMongoService<AgentDocument> {
outputs: [],
}))
: createDto.capabilities);
const incomingExecutionAuth = (createDto as any).executionAuth as Record<string, unknown> | undefined;
const existingExecutionAuth = (existing as any)?.executionAuth as Record<string, unknown> | undefined;
const rawExecutionAuth = incomingExecutionAuth ?? existingExecutionAuth;
const modeCandidate = String(rawExecutionAuth?.mode || 'none');
const normalizedExecutionAuthMode: AgentExecutionAuthMode =
modeCandidate === 'platform_token' || modeCandidate === 'external_oauth' || modeCandidate === 'mtls'
? modeCandidate
: 'none';

const normalizedExecutionAuth = {
required: Boolean(rawExecutionAuth?.required),
mode: normalizedExecutionAuthMode,
requiredScopes: Array.isArray(rawExecutionAuth?.requiredScopes)
? rawExecutionAuth.requiredScopes.filter((value: unknown) => typeof value === 'string')
: undefined,
tokenTtlSeconds:
typeof rawExecutionAuth?.tokenTtlSeconds === 'number'
? Math.max(60, Math.min(3600, rawExecutionAuth.tokenTtlSeconds))
: undefined,
dpopRequired: Boolean(rawExecutionAuth?.dpopRequired),
discoveryUrl:
typeof rawExecutionAuth?.discoveryUrl === 'string' && rawExecutionAuth.discoveryUrl.trim().length > 0
? rawExecutionAuth.discoveryUrl
: undefined,
};

return {
...createDto,
Expand Down Expand Up @@ -435,6 +460,20 @@ export class AgentsService extends BaseMongoService<AgentDocument> {
mcpEndpoint: createDto.mcpEndpoint ?? existing?.mcpEndpoint,
githubAppId: createDto.githubAppId ?? existing?.githubAppId,
a2aEndpoint: createDto.a2aEndpoint ?? existing?.a2aEndpoint,
executionAuth: normalizedExecutionAuth,
claudeManaged: (() => {
const raw = (createDto as any).claudeManaged ?? (existing as any)?.claudeManaged;
if (!raw || typeof raw.agentId !== 'string' || !raw.agentId.trim()) return undefined;
return {
agentId: raw.agentId.trim(),
environmentId: typeof raw.environmentId === 'string' ? raw.environmentId.trim() : '',
anthropicModel: typeof raw.anthropicModel === 'string' && raw.anthropicModel.trim() ? raw.anthropicModel.trim() : undefined,
permissionPolicy: raw.permissionPolicy === 'always_allow' || raw.permissionPolicy === 'always_ask' ? raw.permissionPolicy : undefined,
skillIds: Array.isArray(raw.skillIds)
? raw.skillIds.filter((s: unknown) => typeof s === 'string').slice(0, 20)
: undefined,
};
})(),
manifestUrl: createDto.manifestUrl ?? existing?.manifestUrl,
status: AgentStatus.PENDING,
};
Expand Down
81 changes: 80 additions & 1 deletion apps/platform-api/src/app/agents/dto/register-agent.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsArray, IsEnum, IsNumber, IsOptional, IsUrl, ValidateNested, IsObject, Min, Max } from 'class-validator';
import { IsString, IsNotEmpty, IsArray, IsEnum, IsNumber, IsOptional, IsUrl, ValidateNested, IsObject, Min, Max, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
import { AgentStatus } from '@wuselverse/contracts';

Expand Down Expand Up @@ -111,6 +111,73 @@ export class AgentPricingDto {
outcomes?: OutcomePricingDto[];
}

export class ExecutionAuthDto {
@ApiPropertyOptional({ description: 'Whether off-platform execution authentication is required', default: false })
@IsOptional()
@IsBoolean()
required?: boolean;

@ApiPropertyOptional({
enum: ['none', 'platform_token', 'external_oauth', 'mtls'],
description: 'Execution auth mode advertised to consumers',
default: 'none',
})
@IsOptional()
@IsEnum(['none', 'platform_token', 'external_oauth', 'mtls'])
mode?: 'none' | 'platform_token' | 'external_oauth' | 'mtls';

@ApiPropertyOptional({ type: [String], description: 'Requested scopes for execution-time auth' })
@IsOptional()
@IsArray()
@IsString({ each: true })
requiredScopes?: string[];

@ApiPropertyOptional({ description: 'Requested token TTL in seconds', minimum: 60, maximum: 3600 })
@IsOptional()
@IsNumber()
@Min(60)
@Max(3600)
tokenTtlSeconds?: number;

@ApiPropertyOptional({ description: 'Whether DPoP proof-of-possession is required', default: false })
@IsOptional()
@IsBoolean()
dpopRequired?: boolean;

@ApiPropertyOptional({ description: 'Optional discovery URL for auth setup' })
@IsOptional()
@IsUrl({ require_tld: false })
discoveryUrl?: string;
}

export class ClaudeManagedRuntimeDto {
@ApiProperty({ description: 'Anthropic Managed Agents agent ID (e.g. ant_agent_...)' })
@IsString()
@IsNotEmpty()
agentId: string;

@ApiProperty({ description: 'Anthropic environment ID for session provisioning' })
@IsString()
@IsNotEmpty()
environmentId: string;

@ApiPropertyOptional({ description: 'Anthropic model override (e.g. claude-opus-4-7)' })
@IsOptional()
@IsString()
anthropicModel?: string;

@ApiPropertyOptional({ enum: ['always_allow', 'always_ask'], description: 'Permission policy for CMA tool calls' })
@IsOptional()
@IsEnum(['always_allow', 'always_ask'])
permissionPolicy?: 'always_allow' | 'always_ask';

@ApiPropertyOptional({ type: [String], description: 'Anthropic pre-built or custom skill IDs (max 20 per session)' })
@IsOptional()
@IsArray()
@IsString({ each: true })
skillIds?: string[];
}

class ReputationDto {
@ApiProperty({ description: 'Reputation score (0-100)', minimum: 0, maximum: 100, example: 85 })
@IsNumber()
Expand Down Expand Up @@ -260,6 +327,18 @@ export class RegisterAgentDto {
@IsUrl()
a2aEndpoint?: string;

@ApiPropertyOptional({ type: ExecutionAuthDto, description: 'Optional execution auth requirements for off-platform task execution' })
@IsOptional()
@ValidateNested()
@Type(() => ExecutionAuthDto)
executionAuth?: ExecutionAuthDto;

@ApiPropertyOptional({ type: () => ClaudeManagedRuntimeDto, description: 'Claude Managed Agents runtime configuration' })
@IsOptional()
@ValidateNested()
@Type(() => ClaudeManagedRuntimeDto)
claudeManaged?: ClaudeManagedRuntimeDto;

@ApiPropertyOptional({ description: 'URL to full Agent Service Manifest' })
@IsOptional()
@IsUrl()
Expand Down
15 changes: 14 additions & 1 deletion apps/platform-api/src/app/agents/dto/update-agent.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IsString, IsOptional, IsObject, ValidateNested } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { AgentPricingDto } from './register-agent.dto';
import { AgentPricingDto, ClaudeManagedRuntimeDto, ExecutionAuthDto } from './register-agent.dto';

export class UpdateAgentDto {
@ApiPropertyOptional({ description: 'Agent name' })
Expand Down Expand Up @@ -44,6 +44,19 @@ export class UpdateAgentDto {
@IsOptional()
a2aEndpoint?: string;

@ApiPropertyOptional({ type: ExecutionAuthDto, description: 'Optional execution auth requirements for off-platform task execution' })
@IsOptional()
@IsObject()
@ValidateNested()
@Type(() => ExecutionAuthDto)
executionAuth?: ExecutionAuthDto;

@ApiPropertyOptional({ type: () => ClaudeManagedRuntimeDto, description: 'Claude Managed Agents runtime configuration' })
@IsOptional()
@ValidateNested()
@Type(() => ClaudeManagedRuntimeDto)
claudeManaged?: ClaudeManagedRuntimeDto;

@ApiPropertyOptional({ description: 'Agent service manifest URL' })
@IsString()
@IsOptional()
Expand Down
2 changes: 2 additions & 0 deletions apps/platform-api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReviewsModule } from './reviews/reviews.module';
import { TransactionsModule } from './transactions/transactions.module';
import { RealtimeModule } from './realtime/realtime.module';
import { AuthModule } from './auth/auth.module';
import { ExecutionModule } from './execution/execution.module';
import { HealthController } from './health.controller';

const isTestEnv = process.env.NODE_ENV === 'test' || Boolean(process.env.JEST_WORKER_ID);
Expand Down Expand Up @@ -47,6 +48,7 @@ const throttlerLimit = Number(process.env.THROTTLE_LIMIT ?? (isTestEnv ? 10000 :
}),
ThrottlerModule.forRoot([{ ttl: throttlerTtlSeconds, limit: throttlerLimit }]),
AuthModule,
ExecutionModule,
RealtimeModule,
AgentsModule,
TasksModule,
Expand Down
5 changes: 3 additions & 2 deletions apps/platform-api/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthService } from './auth.service';
import { UserSchema } from './user.schema';
import { UserSessionSchema } from './user-session.schema';
import { UserApiKeySchema } from './user-api-key.schema';
import { ApiKeyGuard } from './api-key.guard';
import { SessionAuthGuard } from './session-auth.guard';
import { SessionCsrfGuard } from './session-csrf.guard';
import { AnyAuthGuard } from './any-auth.guard';
Expand All @@ -21,7 +22,7 @@ import { AnyAuthGuard } from './any-auth.guard';
AgentsModule,
],
controllers: [AuthController],
providers: [AuthService, SessionAuthGuard, SessionCsrfGuard, AnyAuthGuard],
exports: [AuthService, SessionAuthGuard, SessionCsrfGuard, AnyAuthGuard, MongooseModule],
providers: [AuthService, ApiKeyGuard, SessionAuthGuard, SessionCsrfGuard, AnyAuthGuard],
exports: [AuthService, ApiKeyGuard, SessionAuthGuard, SessionCsrfGuard, AnyAuthGuard, MongooseModule],
})
export class AuthModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsEnum, IsOptional, IsString, Max, Min, IsInt } from 'class-validator';

export class CreateExecutionSessionDto {
@ApiProperty({ description: 'Task ID for which execution auth session is being created' })
@IsString()
taskId: string;

@ApiProperty({ enum: ['consumer', 'provider'], description: 'Role for token issuance' })
@IsEnum(['consumer', 'provider'])
role: 'consumer' | 'provider';

@ApiPropertyOptional({ type: [String], description: 'Requested execution scopes' })
@IsOptional()
@IsArray()
@IsString({ each: true })
scopes?: string[];

@ApiPropertyOptional({ description: 'Optional token audience' })
@IsOptional()
@IsString()
audience?: string;

@ApiPropertyOptional({ description: 'Optional key thumbprint for proof-of-possession binding (cnf.jkt)' })
@IsOptional()
@IsString()
cnfJkt?: string;

@ApiPropertyOptional({ description: 'Token TTL in seconds (default: 600)', minimum: 60, maximum: 3600 })
@IsOptional()
@IsInt()
@Min(60)
@Max(3600)
ttlSeconds?: number;
}
Loading
Loading