Skip to content
Open
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: 8 additions & 0 deletions docs/integrations/feishu-notifications.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ AGENTMESH_FEISHU_SEND_INTERVAL_SECONDS=1

## 可靠性与边界

可信扩展也可通过 `BusinessObjectService.apply_action(activity=BusinessActivityNotice(...))`
报告员工的 `STARTED`、`RESULT`、`FAILED` 工作状态。这个参数不是公开 HTTP API 的输入字段。
通知只允许员工标识、角色名和最长 500 字的明确业务摘要,不接收原始记忆、提示词或推理。
活动事件和业务版本在同一事务提交;飞书投递仍要求原通知 Gate 与协作同步同时开启。
通知进程核对租户、公司、对象、确切版本和内容指纹,再按已有员工机器人绑定发送。
默认不发送摘要内容;开启 `FEISHU_INCLUDE_CONTENT` 后仍应用凭据检查。此机制不依赖任何业务场景,
也不新增公网回调、群消息读取、自动批准或外部发布能力。

[真实验收记录](../qualification/feishu-collaboration.md)记录了四员工真实模型协作及九条飞书消息
成功接收的结果,并明确验收范围和未覆盖的边界。

Expand Down
65 changes: 31 additions & 34 deletions src/agentmesh/application/business_object_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from uuid import UUID

from agentmesh.application.ports import UnitOfWorkFactory
from agentmesh.domain.business_activity import ACTIVITY_SCHEMA, BusinessActivityNotice
from agentmesh.domain.business_objects import (
BusinessObject,
BusinessObjectRevision,
Expand Down Expand Up @@ -70,9 +71,7 @@ def create_type(self, company_id: UUID, **values: Any) -> BusinessObjectType:
uow.commit()
return object_type

def transition_type(
self, company_id: UUID, type_id: UUID, action: str
) -> BusinessObjectType:
def transition_type(self, company_id: UUID, type_id: UUID, action: str) -> BusinessObjectType:
self._require_enabled()
with self._uow_factory() as uow:
self._active_company(uow, company_id)
Expand All @@ -92,9 +91,7 @@ def transition_type(
elif action == "deprecate":
object_type.deprecate()
else:
raise InvalidBusinessObject(
f"Unknown Business Object Type action '{action}'"
)
raise InvalidBusinessObject(f"Unknown Business Object Type action '{action}'")
uow.business_objects.save_type(object_type)
self._emit(
uow,
Expand Down Expand Up @@ -141,13 +138,8 @@ def create_object(
"Only a published Business Object Type can create Objects"
)
validate_data(object_type.json_schema, data, label="Business Object data")
if (
object_type.ownership_rules.get("position_required")
and owner_position_id is None
):
raise InvalidBusinessObject(
"Business Object Type requires an owner Position"
)
if object_type.ownership_rules.get("position_required") and owner_position_id is None:
raise InvalidBusinessObject("Business Object Type requires an owner Position")
self._validate_owner(uow, company_id, owner_position_id)
if external_ref and uow.business_objects.get_object_by_external_ref(
type_id, external_ref.strip()
Expand Down Expand Up @@ -199,6 +191,7 @@ def apply_action(
evidence_refs: list[str] | None = None,
actor_position_key: str | None = None,
actor_capabilities: list[str] | None = None,
activity: BusinessActivityNotice | None = None,
) -> BusinessObjectSnapshot:
self._require_enabled()
with self._uow_factory() as uow:
Expand Down Expand Up @@ -234,22 +227,16 @@ def apply_action(
)
evidence = sorted(set(evidence_refs or []))
if action["required_evidence"] and not evidence:
raise BusinessObjectConflict(
f"Action '{action_key}' requires evidence references"
)
raise BusinessObjectConflict(f"Action '{action_key}' requires evidence references")
required_positions = set(action["required_position_keys"])
if required_positions and actor_position_key not in required_positions:
raise BusinessObjectConflict(
f"Action '{action_key}' requires an allowed Position"
)
raise BusinessObjectConflict(f"Action '{action_key}' requires an allowed Position")
required_capabilities = set(action["required_capabilities"])
if not required_capabilities <= set(actor_capabilities or []):
raise BusinessObjectConflict(
f"Action '{action_key}' requires declared capabilities"
)
previous = uow.business_objects.get_revision(
value.id, value.current_revision
)
previous = uow.business_objects.get_revision(value.id, value.current_revision)
if previous is None:
raise BusinessObjectNotFound("Current Business Object revision was not found")
next_data = {**previous.data, **dict(input)}
Expand All @@ -258,9 +245,7 @@ def apply_action(
next_data,
label="Resulting Business Object data",
)
value.apply(
expected_revision=expected_revision, target_state=action["to"]
)
value.apply(expected_revision=expected_revision, target_state=action["to"])
revision = BusinessObjectRevision.create(
object_id=value.id,
revision=value.current_revision,
Expand All @@ -285,13 +270,29 @@ def apply_action(
"side_effect_class": action["side_effect_class"],
},
)
if activity is not None:
# The notice and exact object revision commit together. API requests
# cannot supply this parameter: it is a trusted extension SDK hook.
notice = BusinessActivityNotice.model_validate(activity)
uow.outbox.add(
MessageEnvelope.domain_event(
schema_name=ACTIVITY_SCHEMA,
tenant_id=self._tenant_id,
aggregate_id=value.id,
payload={
"company_id": str(company_id),
"object_id": str(value.id),
"revision": revision.revision,
"data_digest": revision.data_digest,
"activity": notice.model_dump(),
},
)
)
uow.commit()
revisions = uow.business_objects.list_revisions(value.id)
return self._snapshot(value, object_type, revisions)

def get_object(
self, company_id: UUID, object_id: UUID
) -> BusinessObjectSnapshot:
def get_object(self, company_id: UUID, object_id: UUID) -> BusinessObjectSnapshot:
self._require_enabled()
with self._uow_factory() as uow:
self._company(uow, company_id)
Expand Down Expand Up @@ -332,9 +333,7 @@ def _validate_owner(uow: Any, company_id: UUID, owner_position_id: UUID | None)
or position.company_id != company_id
or position.status is not ResourceStatus.ACTIVE
):
raise InvalidBusinessObject(
"Business Object owner must be an active Company Position"
)
raise InvalidBusinessObject("Business Object owner must be an active Company Position")

@staticmethod
def _event_payload(
Expand Down Expand Up @@ -369,9 +368,7 @@ def _snapshot(
)
for revision in revisions
]
return BusinessObjectSnapshot(
object=value, type=object_type, revisions=redacted
)
return BusinessObjectSnapshot(object=value, type=object_type, revisions=redacted)

def _company(self, uow: Any, company_id: UUID):
company = uow.company_model.get_company(company_id)
Expand Down
24 changes: 23 additions & 1 deletion src/agentmesh/application/identity_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import hashlib
import hmac
import json
from dataclasses import dataclass
from dataclasses import dataclass, replace
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import UUID
Expand Down Expand Up @@ -119,6 +119,28 @@ def authorize(self, principal: PrincipalContext, permission: Permission) -> None
f"Principal '{principal.principal_id}' lacks permission '{permission.value}'"
)

def resolve_configured_delegate(self, principal_id: str) -> PrincipalContext:
"""Trusted in-process adapters only; not an authentication endpoint.

The adapter must authenticate its external transport and explicitly bind
the external human to a deployment-configured principal. No role supplied
by an event is trusted, and persistent roles are read on every dispatch.
This avoids copying an operator's bearer credential into an adapter.
"""
matched = next((p for p in self._principals if p.principal_id == principal_id), None)
if not self.enabled or matched is None:
raise AuthenticationFailed("Configured delegate is unavailable")
self._validate_configured(matched)
if self.persistent:
try:
identity = UUID(principal_id)
except ValueError as exc:
raise AuthenticationFailed("Configured delegate ID must be a UUID") from exc
return self._persistent_context(identity, "trusted_adapter_binding")
return replace(
self._configured_context(matched), authentication_method="trusted_adapter_binding"
)

def local_context(self) -> PrincipalContext:
return PrincipalContext(
principal_id="local-development",
Expand Down
27 changes: 27 additions & 0 deletions src/agentmesh/domain/business_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Explicit, bounded employee activity notices supplied by trusted extensions.

Only the summary is intended for external collaboration channels; never pass raw
prompts, memory, model reasoning, credentials, or provider responses here.
"""

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator

ACTIVITY_SCHEMA = "agentmesh.company.object.activity"


class BusinessActivityNotice(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)

agent_id: str = Field(min_length=1, max_length=128)
role_label: str = Field(min_length=1, max_length=80)
phase: Literal["STARTED", "RESULT", "FAILED"]
summary: str | None = Field(default=None, min_length=1, max_length=500)

@field_validator("agent_id", "role_label", "summary")
@classmethod
def normalized_text(cls, value: str | None) -> str | None:
if value is not None and (value != value.strip() or any(ord(c) < 32 for c in value)):
raise ValueError("Activity text must be normalized and contain no control characters")
return value
20 changes: 19 additions & 1 deletion src/agentmesh/infrastructure/postgres/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy.orm import Session

from agentmesh.domain.budgets import BudgetSettlementSource, TaskBudget
from agentmesh.domain.business_activity import ACTIVITY_SCHEMA, BusinessActivityNotice
from agentmesh.domain.coordination import (
CoordinationRuntimeDrain,
CoordinationRuntimeDrainStatus,
Expand Down Expand Up @@ -1065,8 +1066,9 @@ def _to_domain(record: UsageRecordModel) -> UsageRecord:


class SqlAlchemyOutboxRepository:
def __init__(self, session: Session) -> None:
def __init__(self, session: Session, *, feishu_collaboration_enabled: bool = False) -> None:
self._session = session
self._feishu_collaboration_enabled = feishu_collaboration_enabled

def add(self, envelope: MessageEnvelope) -> None:
self._session.add(
Expand All @@ -1086,6 +1088,22 @@ def add(self, envelope: MessageEnvelope) -> None:
last_error=None,
)
)
if self._feishu_collaboration_enabled and envelope.schema_name == ACTIVITY_SCHEMA:
notice = BusinessActivityNotice.model_validate(envelope.payload["activity"])
self._session.add(
FeishuNotificationRecord(
id=uuid5(NAMESPACE_URL, f"agentmesh:feishu:activity:{envelope.message_id}"),
tenant_id=envelope.tenant_id,
subject_type="BUSINESS_ACTIVITY",
subject_id=envelope.message_id,
subject_revision=envelope.payload["revision"],
event_kind=f"ACTIVITY_{notice.phase}",
status="PENDING",
created_at=envelope.occurred_at,
available_at=envelope.occurred_at,
attempt_count=0,
)
)

def add_if_absent(self, envelope: MessageEnvelope) -> bool:
if self._session.get(OutboxEventRecord, envelope.message_id) is not None:
Expand Down
4 changes: 3 additions & 1 deletion src/agentmesh/infrastructure/postgres/uow.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ def __enter__(self) -> SqlAlchemyUnitOfWork:
self.runtime_comparisons = SqlAlchemyRuntimeComparisonRepository(self._session)
self.attempts = SqlAlchemyTaskAttemptRepository(self._session)
self.quotas = SqlAlchemyQuotaRepository(self._session)
self.outbox = SqlAlchemyOutboxRepository(self._session)
self.outbox = SqlAlchemyOutboxRepository(
self._session, feishu_collaboration_enabled=self._feishu_collaboration_enabled
)
self.inbox = SqlAlchemyInboxRepository(self._session)
self.idempotency = SqlAlchemyIdempotencyRepository(self._session)
self.agent_definitions = SqlAlchemyAgentDefinitionRepository(self._session)
Expand Down
Loading
Loading