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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ AGENTMESH_FEATURE_PROFILE=minimal
# Market Intelligence Studio installer:
# AGENTMESH_FEATURE_GATES=company_model=true,business_objects=true,company_packs=true
AGENTMESH_FEATURE_GATES=
# Comma-separated trusted in-process extensions. Empty disables every installed extension.
AGENTMESH_RUNTIME_EXTENSIONS=agentmesh.music-studio
# Required only when identity_rbac is explicitly enabled. Store SHA-256 digests, never raw tokens.
AGENTMESH_IDENTITY_PRINCIPALS_JSON=[]
# Explicit opt-in persistent identity can additionally verify registered OIDC subjects.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ release tags and PEP 440 for the Python package.
discoverable scenario package while retaining its existing API and compatibility import.
- Scenario-owned Music Studio runtime, provider adapters, HTTP routes, and workspace assets with
unchanged public URLs and compatibility re-exports for pre-alpha Python imports.
- Trusted in-process Runtime Extension API v0.1 with entry-point discovery, manifest and collision
validation, capability-limited service factories, explicit enablement, lifecycle health, and
fail-closed Music Studio integration.

## 0.1.0-alpha.1 — 2026-07-27

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ manifest, and SHA-256-linked release manifest. It uses no model or music API key
external request. The focused workspace defaults to English and can switch to Chinese; low-level
Tasks, Runs, Artifacts, and governance records remain in the Admin Console.

Music Studio is loaded through the trusted in-process Runtime Extension API rather than imported
by the core application. `AGENTMESH_RUNTIME_EXTENSIONS=agentmesh.music-studio` is the default;
set it to an empty value to disable every installed extension. Inspect effective versions,
required Features/Credentials, permissions, workspaces, and health at `GET /api/v1/extensions`.
Third-party trusted Python packages can publish the `agentmesh.runtime_extensions` entry-point
group. See the [Runtime Extension Protocol](docs/architecture/modules/runtime-extension-protocol.md).

The same gates expose the built-in **Market Intelligence Studio** in the Admin Console's
**Company** tab. Previewing it shows all 32 resource mutations, permissions, credentials, and the
external-write boundary. One click creates the Company, eight departments, 17 Positions, seven
Expand Down
5 changes: 5 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ AGENTMESH_FEATURE_PROFILE=full
AGENTMESH_FEATURE_GATES=reviewed_execution=true,coordinated_execution=true,agent_registry_management=true,artifact_service=true,mcp_read_tools=true,observability=true,budget_admission=true
```

Music Studio 现在通过受信任的进程内 Runtime Extension API 加载,而不是由核心应用直接导入。
默认配置为 `AGENTMESH_RUNTIME_EXTENSIONS=agentmesh.music-studio`;将其设为空可禁用全部已安装
扩展。`GET /api/v1/extensions` 可以查看版本、健康状态、所需 Feature/API Key、权限和工作区。
扩展开发说明见 [Runtime Extension Protocol](docs/architecture/modules/runtime-extension-protocol.md)。

Identity/RBAC 在所有内置 Profile 中都保持关闭,必须由部署者显式配置凭据后开启。

第一个 Virtual Company 模块需要显式开启:
Expand Down
1 change: 1 addition & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ x-agentmesh-environment: &agentmesh-environment
# Keep the minimal profile, but make the first-run Console demonstrate a real
# multi-Agent DAG. Operators can still override this with an explicit value.
AGENTMESH_FEATURE_GATES: ${AGENTMESH_FEATURE_GATES:-coordinated_execution=true}
AGENTMESH_RUNTIME_EXTENSIONS: ${AGENTMESH_RUNTIME_EXTENSIONS:-agentmesh.music-studio}
AGENTMESH_IDENTITY_PRINCIPALS_JSON: ${AGENTMESH_IDENTITY_PRINCIPALS_JSON:-[]}
AGENTMESH_IDENTITY_OIDC_ISSUER: ${AGENTMESH_IDENTITY_OIDC_ISSUER:-}
AGENTMESH_IDENTITY_OIDC_AUDIENCE: ${AGENTMESH_IDENTITY_OIDC_AUDIENCE:-}
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The complete target design is maintained separately:

Formal module documents:

- [Runtime Extension Protocol](runtime-extension-protocol.md)

- [Task and execution domain](formal/task-and-execution-domain.md)
- [Persistence and consistency](formal/persistence-and-consistency.md)
- [Orchestrator and scheduler](formal/orchestrator-and-scheduler.md)
Expand Down
11 changes: 6 additions & 5 deletions docs/architecture/modules/pack-sdk-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ that exposes the same definition contract after a separate trust and loading des
## Remaining separation work

Phases 1 and 2 separate both the declarative contract and the complete Music Studio implementation
inside the Python package. A later phase can publish that directory as a separately versioned
distribution once the runtime extension protocol and signed external Pack loading/trust model are
stable. The older Market Intelligence template and Operations helpers must also move onto the same
definition contract before the whole application layer is scenario-neutral. Until then, physical
repository separation would create release coupling without a safe installation boundary.
inside the Python package. Runtime Extension API v0.1 now discovers its executable surface through
a generic trusted in-process contract, so the core application and bootstrap no longer import
Music Studio code. A later phase can publish the scenario as a separately versioned distribution
after the signed installation/trust model is stable. The older Market Intelligence template and
Operations helpers must also move onto the same definition contract before the whole application
layer is scenario-neutral.
94 changes: 94 additions & 0 deletions docs/architecture/modules/runtime-extension-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Runtime Extension Protocol

Status: Implemented v0.1 (trusted in-process)

AgentMesh Runtime Extension API v0.1 is the executable boundary between the operating core and a
business scenario. The core owns durable Tasks, Agents, governance, persistence, credentials,
memory, Artifacts, and observability. An extension owns its workflow service, provider adapters,
HTTP API, focused workspace, health probe, and Company Template definitions.

The contract lives in `agentmesh.extensions.sdk`. The current API version is `0.1`.

## Contract

A `RuntimeExtensionDefinition` provides:

- an immutable `ExtensionManifest`;
- a service factory receiving an `ExtensionContext`;
- an API registrar for routes and workspace assets;
- zero or more `CompanyTemplateDefinition` values;
- a health probe and idempotent stop callback.

The manifest declares the extension identifier and semantic version, Runtime API version, required
core services, provided namespaced services, Features, Credentials, permissions, workspaces, and
whether external writes are enabled. AgentMesh validates identifiers, versions, Feature names,
service surfaces, duplicate extension IDs, and extension/core route or workspace/asset collisions
before serving work.

The service factory cannot receive the `ApplicationContainer` or database engine. It receives a
read-only mapping of explicitly exposed core capabilities through stable `CoreServiceKey` names.
This is an API boundary, not a process sandbox.

## Discovery and enablement

Built-in and installed extensions share `RuntimeExtensionRegistry`. A separately distributed
Python package publishes this entry point:

```toml
[project.entry-points."agentmesh.runtime_extensions"]
my_scenario = "my_scenario.extension:EXTENSION"
```

Only packages explicitly installed by the operator are discoverable. Installed entry points are
trusted Python code and are imported during discovery, so operators must not install untrusted
extension packages. Runtime enablement controls service creation and use; it is not a defense
against malicious package import-time code.

Enabled extensions are configured as a comma-separated allowlist:

```dotenv
AGENTMESH_RUNTIME_EXTENSIONS=agentmesh.music-studio
```

An empty value disables all installed extensions. `*` enables all discovered extensions and cannot
be combined with explicit identifiers. Unknown identifiers fail startup instead of being ignored.

## Lifecycle

```text
discover -> validate -> load services -> probe -> serve -> stop
```

API routes are registered from validated definitions while constructing the FastAPI application.
An enabled extension then creates exactly its declared service keys. Disabled extensions remain
visible to operators but service and workspace access fails closed with HTTP 503. Missing optional
Feature activation produces a `degraded` status; route-level Feature gates still enforce the
operation. `ApplicationContainer.close()` invokes every loaded extension stop callback once.

Operators can inspect the effective state at `GET /api/v1/extensions`. The response discloses
version, health, missing Features, required Credentials and permissions, service keys, workspace
routes, and the external-write boundary.

## Music Studio proof

Music Studio is the first implementation. The core API and bootstrap modules do not import its
runtime, routes, console, or provider code. The built-in registry discovers one definition, the
generic runtime supplies its required services, and its registrar preserves these URLs:

- `/music-studio`;
- `/console/assets/music-studio.*`;
- `/api/v1/music-studio/*`.

Its Company Template is also projected into `PackCatalog`, so declarative installation and
executable runtime discovery come from one scenario definition.

## Deliberate v0.1 limits

- extensions run in the AgentMesh API process and have the privileges of that process;
- installing, removing, or changing the allowlist requires a restart;
- extension-owned database migrations are not accepted;
- signed bundles, registry trust policy, dependency resolution, hot reload, process isolation, and
remote A2A extensions remain later protocol versions.

The next security step should be a signed installation/preflight model. Process isolation should
only follow after the in-process contract is proven by a second independently maintained scenario.
14 changes: 14 additions & 0 deletions docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ Company Pack SDK separation phase 2 on 2026-08-03 additionally:
- left external executable Pack loading deliberately unsupported until a signed trust and runtime
extension protocol is specified.

Runtime Extension API v0.1 on 2026-08-03 additionally:

- passed 426 non-PostgreSQL tests at 83.25% line coverage (gate: 80%);
- introduced a trusted in-process extension manifest, registry, entry-point discovery, controlled
core-service context, exact service-surface validation, health probe, and stop lifecycle;
- added explicit `AGENTMESH_RUNTIME_EXTENSIONS` allowlisting, fail-fast unknown identifiers, and
fail-closed disabled service/workspace access;
- exposed `GET /api/v1/extensions` for version, health, Feature, Credential, permission, workspace,
service, and external-write disclosure;
- migrated Music Studio to the generic runtime so the core API/bootstrap no longer imports its
routes, console, runtime service, or providers;
- kept installed Python extensions explicitly trusted and deferred signatures, sandboxing, hot
reload, extension migrations, and remote execution to later protocol revisions.

Market Intelligence Studio baseline verification on 2026-07-30 additionally:

- passed 390 non-PostgreSQL tests at 82.71% line coverage (gate: 80%);
Expand Down
15 changes: 10 additions & 5 deletions src/agentmesh/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from agentmesh.api.console import register_console
from agentmesh.api.credential_routes import router as credential_router
from agentmesh.api.event_routes import router as event_router
from agentmesh.api.extension_routes import router as extension_router
from agentmesh.api.feature_routes import router as feature_router
from agentmesh.api.financial_governance_routes import (
router as financial_governance_router,
Expand Down Expand Up @@ -110,8 +111,8 @@
TaskNotFound,
ToolInvocationFailed,
)
from agentmesh.packs.music_studio.console import register_music_studio_console
from agentmesh.packs.music_studio.routes import router as music_studio_router
from agentmesh.extensions.builtin import RUNTIME_EXTENSION_REGISTRY
from agentmesh.extensions.sdk import RuntimeExtensionUnavailable


def create_app(container: ApplicationContainer | None = None) -> FastAPI:
Expand Down Expand Up @@ -139,6 +140,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
application.include_router(a2a_router)
application.include_router(credential_router)
application.include_router(event_router)
application.include_router(extension_router)
application.include_router(activity_router)
application.include_router(artifact_router)
application.include_router(business_object_router)
Expand All @@ -151,18 +153,21 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
application.include_router(company_template_router)
application.include_router(mcp_router)
application.include_router(mcp_registry_router)
application.include_router(music_studio_router)
application.include_router(policy_router)
application.include_router(quota_router)
application.include_router(office_router)
# Scenario-owned asset routes must be registered before the core static mount.
register_music_studio_console(application)
# Extension asset routes must be registered before the core static mount.
RUNTIME_EXTENSION_REGISTRY.register_api(application)
register_console(application)
_register_error_handlers(application)
return application


def _register_error_handlers(application: FastAPI) -> None:
application.add_exception_handler(
RuntimeExtensionUnavailable,
lambda request, exc: _error(503, "runtime_extension_unavailable", str(exc)),
)
for error_type in (AuthenticationRequired, AuthenticationFailed):
application.add_exception_handler(
error_type,
Expand Down
70 changes: 70 additions & 0 deletions src/agentmesh/api/extension_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

from fastapi import APIRouter, Request
from pydantic import BaseModel

from agentmesh.extensions.runtime import RuntimeExtensionStatus

router = APIRouter(prefix="/api/v1/extensions", tags=["runtime-extensions"])


class ExtensionWorkspaceResponse(BaseModel):
route: str
name: str
asset_prefix: str | None


class RuntimeExtensionResponse(BaseModel):
identifier: str
name: str
version: str
api_version: str
description: str
enabled: bool
health: str
message: str
required_features: list[str]
missing_features: list[str]
required_credentials: list[str]
permissions: list[str]
provided_services: list[str]
loaded_services: list[str]
workspaces: list[ExtensionWorkspaceResponse]
external_writes_enabled: bool

@classmethod
def from_status(cls, status: RuntimeExtensionStatus) -> RuntimeExtensionResponse:
manifest = status.manifest
return cls(
identifier=manifest.identifier,
name=manifest.name,
version=manifest.version,
api_version=manifest.api_version,
description=manifest.description,
enabled=status.enabled,
health=status.health,
message=status.message,
required_features=list(manifest.required_features),
missing_features=list(status.missing_features),
required_credentials=list(manifest.required_credentials),
permissions=list(manifest.permissions),
provided_services=list(manifest.provided_services),
loaded_services=list(status.service_keys),
workspaces=[
ExtensionWorkspaceResponse(
route=item.route,
name=item.name,
asset_prefix=item.asset_prefix,
)
for item in manifest.workspaces
],
external_writes_enabled=manifest.external_writes_enabled,
)


@router.get("", response_model=list[RuntimeExtensionResponse])
def list_runtime_extensions(request: Request) -> list[RuntimeExtensionResponse]:
return [
RuntimeExtensionResponse.from_status(item)
for item in request.app.state.container.extension_runtime.statuses()
]
34 changes: 24 additions & 10 deletions src/agentmesh/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
from agentmesh.domain.model_runtime import ModelRuntimePolicy
from agentmesh.domain.pricing import UsagePriceCatalog
from agentmesh.domain.tools import WORKSPACE_READ_TOOL_KEY, ToolBinding, ToolSideEffect
from agentmesh.extensions.builtin import RUNTIME_EXTENSION_REGISTRY
from agentmesh.extensions.runtime import ExtensionRuntime
from agentmesh.extensions.sdk import CoreServiceKey, ExtensionContext
from agentmesh.features import Feature, FeatureGateSet
from agentmesh.infrastructure.artifact_storage import LocalArtifactBlobStore
from agentmesh.infrastructure.postgres.office_repositories import (
Expand Down Expand Up @@ -95,7 +98,6 @@
VersionBoundAgentExecutor,
)
from agentmesh.orchestration.workflow import LangGraphWorkflowRunner
from agentmesh.packs.music_studio.runtime import MusicStudioService
from agentmesh.workers.a2a_reconciliation import A2AReconciliationWorker
from agentmesh.workers.execution import RedisRunWorker

Expand Down Expand Up @@ -132,12 +134,13 @@ class ApplicationContainer:
company_pack_service: CompanyPackService
market_research_service: MarketResearchService
research_materialization_service: ResearchMaterializationService
music_studio_service: MusicStudioService
extension_runtime: ExtensionRuntime
mcp_catalog_client: OfficialMcpRegistryClient | None = None
event_stream: RedisDomainEventStream | None = None
close_callback: Callable[[], None] = lambda: None

def close(self) -> None:
self.extension_runtime.close()
self.close_callback()


Expand Down Expand Up @@ -404,13 +407,24 @@ def build_api_container(settings: Settings | None = None) -> ApplicationContaine
artifact_service=artifact_service,
tenant_id=runtime_settings.tenant_id,
)
music_studio_service = MusicStudioService(
uow_factory=uow_factory,
task_service=task_service,
registry_service=registry_service,
business_object_service=business_object_service,
artifact_service=artifact_service,
tenant_id=runtime_settings.tenant_id,
extension_runtime = ExtensionRuntime.load(
RUNTIME_EXTENSION_REGISTRY,
ExtensionContext(
tenant_id=runtime_settings.tenant_id,
services={
CoreServiceKey.UNIT_OF_WORK_FACTORY.value: uow_factory,
CoreServiceKey.TASKS.value: task_service,
CoreServiceKey.AGENT_REGISTRY.value: registry_service,
CoreServiceKey.ARTIFACTS.value: artifact_service,
CoreServiceKey.BUSINESS_OBJECTS.value: business_object_service,
CoreServiceKey.COMPANY_PACKS.value: company_pack_service,
CoreServiceKey.CREDENTIALS.value: credential_broker_service,
CoreServiceKey.MEMORY.value: organizational_memory_service,
CoreServiceKey.POLICIES.value: policy_service,
},
),
feature_gates,
runtime_settings.runtime_extensions,
)

def close() -> None:
Expand Down Expand Up @@ -449,7 +463,7 @@ def close() -> None:
company_pack_service=company_pack_service,
market_research_service=market_research_service,
research_materialization_service=research_materialization_service,
music_studio_service=music_studio_service,
extension_runtime=extension_runtime,
mcp_catalog_client=OfficialMcpRegistryClient(),
event_stream=event_stream,
close_callback=close,
Expand Down
1 change: 1 addition & 0 deletions src/agentmesh/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Settings(BaseSettings):
langfuse_timeout_seconds: int = Field(default=5, ge=1, le=60)
feature_profile: str = "minimal"
feature_gates: str = ""
runtime_extensions: str = "agentmesh.music-studio"
operations_batch_size: int = Field(default=50, ge=1, le=500)
operations_scan_seconds: int = Field(default=5, ge=1, le=300)
identity_principals_json: str = "[]"
Expand Down
Loading