fix(agent): preserve DSL asset placeholders and count draft references (#39015)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-07-15 19:49:11 +08:00 committed by GitHub
parent dcbedbd3bc
commit 73c3048599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 524 additions and 55 deletions

View File

@ -241,6 +241,7 @@ class AgentAppPartial(GenericAppPartial):
debug_conversation_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
reference_count: int | None = None
published_reference_count: int = 0
published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list)
@ -427,6 +428,10 @@ def _serialize_agent_app_pagination(session: Session, app_pagination, *, tenant_
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents_by_app_id.values()],
)
reference_counts_by_agent_id = roster_service.load_reference_counts_by_agent_id(
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents_by_app_id.values()],
)
debug_conversation_ids_by_agent_id = roster_service.load_or_create_agent_app_debug_conversation_ids_by_agent_id(
tenant_id=tenant_id,
agents=list(agents_by_app_id.values()),
@ -449,6 +454,7 @@ def _serialize_agent_app_pagination(session: Session, app_pagination, *, tenant_
item["debug_conversation_id"] = debug_conversation_ids_by_agent_id.get(agent.id)
item["role"] = agent.role or ""
item["active_config_is_published"] = active_config_is_published_by_agent_id.get(agent.id, False)
item["reference_count"] = reference_counts_by_agent_id.get(agent.id, 0)
published_references = published_references_by_agent_id.get(agent.id, [])
item["published_reference_count"] = len(published_references)
item["published_references"] = [

View File

@ -100,6 +100,7 @@ class AgentConfigSkillItemResponse(ResponseModel):
id: str
name: str
file_id: str | None = None
is_missing: bool = False
description: str = ""
size: int | None = None
mime_type: str | None = None
@ -110,6 +111,7 @@ class AgentConfigFileItemResponse(ResponseModel):
id: str
name: str
file_id: str | None = None
is_missing: bool = False
size: int | None = None
mime_type: str | None = None
hash: str | None = None

View File

@ -887,8 +887,8 @@ def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
"""Resolve config skill/file mentions and delegate the rest to Agent Soul."""
base_resolver = build_soul_mention_resolver(agent_soul)
skill_names = {item.name for item in agent_soul.config_skills}
file_names = {item.name for item in agent_soul.config_files}
skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
def _resolve(mention: object) -> str | None:
if not hasattr(mention, "kind") or not hasattr(mention, "ref_id"):
@ -926,9 +926,20 @@ def build_config_layer_config(
if mention.kind in {MentionKind.SKILL, MentionKind.FILE} and mention.ref_id
)
)
skill_names = {skill.name for skill in agent_soul.config_skills}
file_names = {file_ref.name for file_ref in agent_soul.config_files}
warnings: list[dict[str, str]] = []
available_skills = [skill for skill in agent_soul.config_skills if not skill.is_missing]
available_files = [file_ref for file_ref in agent_soul.config_files if not file_ref.is_missing]
skill_names = {skill.name for skill in available_skills}
file_names = {file_ref.name for file_ref in available_files}
warnings: list[dict[str, str]] = [
{
"section": "agent_soul.config",
"code": "config_asset_missing",
"message": f"config {kind} '{item.name}' is unavailable and was excluded from runtime.",
}
for kind, items in (("skill", agent_soul.config_skills), ("file", agent_soul.config_files))
for item in items
if item.is_missing
]
mentioned_skill_names: list[str] = []
mentioned_file_names: list[str] = []
for name in ordered_mentions:
@ -961,7 +972,7 @@ def build_config_layer_config(
size=skill.size,
mime_type=skill.mime_type,
)
for skill in agent_soul.config_skills
for skill in available_skills
],
files=[
DifyConfigFileConfig(
@ -969,7 +980,7 @@ def build_config_layer_config(
size=file_ref.size,
mime_type=file_ref.mime_type,
)
for file_ref in agent_soul.config_files
for file_ref in available_files
],
env_keys=_agent_soul_config_env_keys(agent_soul),
note=agent_soul.config_note,

View File

@ -99,6 +99,7 @@ class AgentRosterResponse(ResponseModel):
archived_at: int | None = None
created_at: int | None = None
updated_at: int | None = None
reference_count: int | None = None
published_reference_count: int = 0
published_node_reference_count: int = 0
published_references: list[AgentPublishedReferenceResponse] = Field(default_factory=list)

View File

@ -195,7 +195,8 @@ class AgentConfigFileRefConfig(BaseModel):
name: str = Field(min_length=1, max_length=255)
file_kind: Literal["upload_file", "tool_file"]
file_id: str = Field(min_length=1, max_length=255)
file_id: str = Field(default="", max_length=255)
is_missing: bool = False
size: int | None = None
hash: str | None = None
mime_type: str | None = None
@ -205,6 +206,16 @@ class AgentConfigFileRefConfig(BaseModel):
def _validate_name(cls, value: str) -> str:
return validate_config_name(value)
@model_validator(mode="after")
def _validate_file_reference(self) -> Self:
if self.is_missing:
if self.file_id:
raise ValueError("missing config files must not retain a workspace-local file_id")
return self
if not self.file_id or not self.file_id.strip():
raise ValueError("config file file_id is required unless is_missing is true")
return self
class AgentConfigSkillRefConfig(BaseModel):
"""Stable Agent Soul reference to one normalized skill archive."""
@ -214,7 +225,8 @@ class AgentConfigSkillRefConfig(BaseModel):
name: str = Field(min_length=1, max_length=255)
description: str = ""
file_kind: Literal["tool_file"] = "tool_file"
file_id: str = Field(min_length=1, max_length=255)
file_id: str = Field(default="", max_length=255)
is_missing: bool = False
size: int | None = None
hash: str | None = None
mime_type: str | None = "application/zip"
@ -224,6 +236,16 @@ class AgentConfigSkillRefConfig(BaseModel):
def _validate_name(cls, value: str) -> str:
return validate_config_skill_name(value)
@model_validator(mode="after")
def _validate_file_reference(self) -> Self:
if self.is_missing:
if self.file_id:
raise ValueError("missing config skills must not retain a workspace-local file_id")
return self
if not self.file_id or not self.file_id.strip():
raise ValueError("config skill file_id is required unless is_missing is true")
return self
class AgentPermissionConfig(BaseModel):
model_config = ConfigDict(extra="ignore")

View File

@ -13282,6 +13282,7 @@ default (the config form sends the full desired feature state on save).
| permission_keys | [ string ] | | No |
| published_reference_count | integer | | No |
| published_references | [ [AgentAppPublishedReferenceResponse](#agentapppublishedreferenceresponse) ] | | No |
| reference_count | integer | | No |
| role | string | | No |
| tags | [ [Tag](#tag) ] | | No |
| updated_at | integer | | No |
@ -13574,6 +13575,7 @@ Editable Agent Soul draft workspace type.
| file_id | string | | No |
| hash | string | | No |
| id | string | | Yes |
| is_missing | boolean | | No |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
@ -13608,9 +13610,10 @@ Stable Agent Soul reference to one config file payload.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| file_id | string | | Yes |
| file_id | string | | No |
| file_kind | string, <br>**Available values:** "tool_file", "upload_file" | *Enum:* `"tool_file"`, `"upload_file"` | Yes |
| hash | string | | No |
| is_missing | boolean | | No |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
@ -13705,6 +13708,7 @@ Audit operation recorded for Agent Soul version/revision changes.
| file_id | string | | No |
| hash | string | | No |
| id | string | | Yes |
| is_missing | boolean | | No |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
@ -13740,9 +13744,10 @@ Stable Agent Soul reference to one normalized skill archive.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | | No |
| file_id | string | | Yes |
| file_id | string | | No |
| file_kind | string, <br>**Default:** tool_file | | No |
| hash | string | | No |
| is_missing | boolean | | No |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
@ -14080,6 +14085,7 @@ Supported icon storage formats for Agent roster entries.
| published_node_reference_count | integer | | No |
| published_reference_count | integer | | No |
| published_references | [ [AgentPublishedReferenceResponse](#agentpublishedreferenceresponse) ] | | No |
| reference_count | integer | | No |
| role | string | | No |
| scope | [AgentScope](#agentscope) | | Yes |
| source | [AgentSource](#agentsource) | | Yes |
@ -14521,6 +14527,7 @@ section may be empty, which is how callers express "no knowledge layer".
| published_node_reference_count | integer | | No |
| published_reference_count | integer | | No |
| published_references | [ [AgentPublishedReferenceResponse](#agentpublishedreferenceresponse) ] | | No |
| reference_count | integer | | No |
| role | string | | No |
| scope | [AgentScope](#agentscope) | | Yes |
| source | [AgentSource](#agentsource) | | Yes |

View File

@ -229,10 +229,20 @@ class ComposerConfigValidator:
@classmethod
def validate_agent_soul(cls, agent_soul: AgentSoulConfig) -> None:
dumped = agent_soul.model_dump(mode="json")
cls._reject_missing_config_assets(agent_soul)
cls._validate_knowledge_runtime_config(agent_soul)
cls._reject_plaintext_secrets(dumped, path="agent_soul")
cls._validate_shell_config(dumped)
@staticmethod
def _reject_missing_config_assets(agent_soul: AgentSoulConfig) -> None:
missing = [f"skill:{item.name}" for item in agent_soul.config_skills if item.is_missing]
missing.extend(f"file:{item.name}" for item in agent_soul.config_files if item.is_missing)
if missing:
raise InvalidComposerConfigError(
"config_asset_missing: upload the missing Agent config assets before publishing: " + ", ".join(missing)
)
@classmethod
def _validate_knowledge_runtime_config(cls, agent_soul: AgentSoulConfig) -> None:
"""Validate knowledge settings that are required only for publish/run.

View File

@ -99,8 +99,12 @@ def make_portable_agent_package(agent: Agent, agent_soul: AgentSoulConfig) -> Ag
)
for item in agent_soul.config_files
)
soul_data["config_skills"] = []
soul_data["config_files"] = []
for item in soul_data.get("config_skills", []):
item["file_id"] = ""
item["is_missing"] = True
for item in soul_data.get("config_files", []):
item["file_id"] = ""
item["is_missing"] = True
if soul_data.get("model"):
soul_data["model"]["credential_ref"] = None

View File

@ -427,7 +427,7 @@ class AgentDslService:
app.updated_by = account.id
self.session.add(app)
self.session.flush()
app_was_created.send(app, account=account)
app_was_created.send(app, account=account, session=self.session)
self._configure_visible_agent_app_after_commit(
tenant_id=tenant_id,
app_id=app.id,

View File

@ -95,6 +95,7 @@ class AgentRosterService:
active_version: AgentConfigSnapshot | None = None,
published_references: list[AgentReferencingWorkflow] | None = None,
active_config_is_published: bool | None = None,
reference_count: int | None = None,
) -> dict[str, Any]:
published_references = published_references or []
return {
@ -126,6 +127,7 @@ class AgentRosterService:
"archived_at": to_timestamp(agent.archived_at),
"created_at": to_timestamp(agent.created_at),
"updated_at": to_timestamp(agent.updated_at),
"reference_count": len(published_references) if reference_count is None else reference_count,
"published_reference_count": len(published_references),
"published_node_reference_count": sum(len(item["node_ids"]) for item in published_references),
"published_references": published_references,
@ -188,6 +190,10 @@ class AgentRosterService:
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents],
)
reference_counts_by_agent_id = self._load_reference_counts_by_agent_id(
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents],
)
active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id(
tenant_id=tenant_id,
agents=agents,
@ -204,6 +210,7 @@ class AgentRosterService:
active_version,
published_references_by_agent_id.get(agent.id, []),
active_config_is_published_by_agent_id.get(agent.id, False),
reference_counts_by_agent_id.get(agent.id, 0),
)
)
@ -230,6 +237,10 @@ class AgentRosterService:
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents],
)
reference_counts_by_agent_id = self._load_reference_counts_by_agent_id(
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents],
)
active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id(
tenant_id=tenant_id,
agents=agents,
@ -240,6 +251,7 @@ class AgentRosterService:
versions_by_id.get(agent.active_config_snapshot_id) if agent.active_config_snapshot_id else None,
published_references_by_agent_id.get(agent.id, []),
active_config_is_published_by_agent_id.get(agent.id, False),
reference_counts_by_agent_id.get(agent.id, 0),
)
for agent in agents
]
@ -1067,6 +1079,10 @@ class AgentRosterService:
"""Return published workflow references grouped by roster Agent id."""
return self._load_published_references_by_agent_id(tenant_id=tenant_id, agent_ids=agent_ids)
def load_reference_counts_by_agent_id(self, *, tenant_id: str, agent_ids: list[str]) -> dict[str, int]:
"""Return current draft or published workflow app references by roster Agent id."""
return self._load_reference_counts_by_agent_id(tenant_id=tenant_id, agent_ids=agent_ids)
def get_roster_agent_detail(self, *, tenant_id: str, agent_id: str) -> dict[str, Any]:
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
active_version = self._get_version(
@ -1076,6 +1092,10 @@ class AgentRosterService:
tenant_id=tenant_id,
agent_ids=[agent.id],
)
reference_counts_by_agent_id = self._load_reference_counts_by_agent_id(
tenant_id=tenant_id,
agent_ids=[agent.id],
)
active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id(
tenant_id=tenant_id,
agents=[agent],
@ -1085,6 +1105,7 @@ class AgentRosterService:
active_version,
published_references_by_agent_id.get(agent.id, []),
active_config_is_published_by_agent_id.get(agent.id, False),
reference_counts_by_agent_id.get(agent.id, 0),
)
def update_roster_agent(
@ -1419,6 +1440,60 @@ class AgentRosterService:
result[agent_id] = references
return result
def _load_reference_counts_by_agent_id(self, *, tenant_id: str, agent_ids: list[str]) -> dict[str, int]:
if not agent_ids:
return {}
bindings = list(
self._session.scalars(
select(WorkflowAgentNodeBinding).where(
WorkflowAgentNodeBinding.tenant_id == tenant_id,
WorkflowAgentNodeBinding.agent_id.in_(agent_ids),
WorkflowAgentNodeBinding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT,
)
).all()
)
if not bindings:
return {}
app_ids = {binding.app_id for binding in bindings}
apps = {
app.id: app
for app in self._session.scalars(
select(App).where(
App.tenant_id == tenant_id,
App.id.in_(app_ids),
App.status == AppStatus.NORMAL,
)
).all()
}
workflow_ids = {binding.workflow_id for binding in bindings}
workflows = {
workflow.id: workflow
for workflow in self._session.scalars(
select(Workflow).where(
Workflow.tenant_id == tenant_id,
Workflow.id.in_(workflow_ids),
)
).all()
}
referenced_app_ids_by_agent_id: dict[str, set[str]] = {}
for binding in bindings:
if not binding.agent_id:
continue
app = apps.get(binding.app_id)
workflow = workflows.get(binding.workflow_id)
if app is None or workflow is None or workflow.app_id != binding.app_id:
continue
if workflow.version != binding.workflow_version:
continue
if workflow.version != Workflow.VERSION_DRAFT and app.workflow_id != workflow.id:
continue
referenced_app_ids_by_agent_id.setdefault(binding.agent_id, set()).add(binding.app_id)
return {agent_id: len(app_ids) for agent_id, app_ids in referenced_app_ids_by_agent_id.items()}
def _load_versions_by_id(self, version_ids: list[str]) -> dict[str, AgentConfigSnapshot]:
if not version_ids:
return {}

View File

@ -200,8 +200,8 @@ class WorkflowAgentPublishService:
from services.agent.prompt_mentions import MentionKind, parse_prompt_mentions
del session
configured_skill_names = {item.name for item in agent_soul.config_skills}
configured_file_names = {item.name for item in agent_soul.config_files}
configured_skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
configured_file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
missing_refs: list[str] = []
for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt):
if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}:

View File

@ -234,7 +234,8 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=skill.file_id)
file_id = self._available_skill_file_id(skill)
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
return ConfigDownload(filename=f"{skill.name}.zip", mime_type=mime_type or "application/zip", payload=payload)
def download_skill_url(
@ -255,7 +256,8 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
url = self._resolve_download_url(tenant_id=tenant_id, file_kind=skill.file_kind, file_id=skill.file_id)
file_id = self._available_skill_file_id(skill)
url = self._resolve_download_url(tenant_id=tenant_id, file_kind=skill.file_kind, file_id=file_id)
if url is None:
raise AgentConfigServiceError("config_skill_not_found", "config skill payload is missing", status_code=404)
return url
@ -278,7 +280,8 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=skill.file_id)
file_id = self._available_skill_file_id(skill)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
try:
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
except (OSError, ValueError, zipfile.BadZipFile) as exc:
@ -314,10 +317,11 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
member_path = self._normalize_archive_member_path(path)
payload = self._load_skill_archive_member(
tenant_id=tenant_id,
file_id=skill.file_id,
file_id=file_id,
path=member_path,
)
return self._preview_bytes(path=member_path, size=len(payload), payload=payload)
@ -341,10 +345,11 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
member_path = self._normalize_archive_member_path(path)
payload = self._load_skill_archive_member(
tenant_id=tenant_id,
file_id=skill.file_id,
file_id=file_id,
path=member_path,
)
return ConfigDownload(
@ -372,10 +377,11 @@ class AgentConfigService:
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
member_path = self._normalize_archive_member_path(path)
self._load_skill_archive_member(
tenant_id=tenant_id,
file_id=skill.file_id,
file_id=file_id,
path=member_path,
)
return member_path
@ -398,10 +404,11 @@ class AgentConfigService:
user_id=user_id,
)
file_ref = self._require_file(target.agent_soul, name=name)
file_id = self._available_file_id(file_ref)
payload, filename, mime_type = self._load_file_ref_bytes(
tenant_id=tenant_id,
file_kind=file_ref.file_kind,
file_id=file_ref.file_id,
file_id=file_id,
)
return ConfigDownload(
filename=filename or file_ref.name, mime_type=mime_type or "application/octet-stream", payload=payload
@ -425,7 +432,8 @@ class AgentConfigService:
user_id=user_id,
)
file_ref = self._require_file(target.agent_soul, name=name)
url = self._resolve_download_url(tenant_id=tenant_id, file_kind=file_ref.file_kind, file_id=file_ref.file_id)
file_id = self._available_file_id(file_ref)
url = self._resolve_download_url(tenant_id=tenant_id, file_kind=file_ref.file_kind, file_id=file_id)
if url is None:
raise AgentConfigServiceError("config_file_not_found", "config file payload is missing", status_code=404)
return url
@ -660,10 +668,11 @@ class AgentConfigService:
user_id=user_id,
)
file_ref = self._require_file(target.agent_soul, name=name)
file_id = self._available_file_id(file_ref)
payload, filename, _mime_type = self._load_file_ref_bytes(
tenant_id=tenant_id,
file_kind=file_ref.file_kind,
file_id=file_ref.file_id,
file_id=file_id,
)
return self._preview_bytes(
path=filename or file_ref.name, size=file_ref.size, payload=payload, field_name="name"
@ -1154,6 +1163,7 @@ class AgentConfigService:
"id": skill.name,
"name": skill.name,
"file_id": skill.file_id,
"is_missing": skill.is_missing,
"description": skill.description,
"size": skill.size,
"hash": skill.hash,
@ -1166,6 +1176,7 @@ class AgentConfigService:
"id": file_ref.name,
"name": file_ref.name,
"file_id": file_ref.file_id,
"is_missing": file_ref.is_missing,
"size": file_ref.size,
"hash": file_ref.hash,
"mime_type": file_ref.mime_type,
@ -1284,6 +1295,12 @@ class AgentConfigService:
normalized = validate_config_skill_name(name)
for item in agent_soul.config_skills:
if item.name == normalized:
if item.is_missing or not item.file_id:
raise AgentConfigServiceError(
"config_skill_missing",
"config skill must be uploaded before it can be used",
status_code=409,
)
return item
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404)
@ -1292,9 +1309,35 @@ class AgentConfigService:
normalized = validate_config_name(name)
for item in agent_soul.config_files:
if item.name == normalized:
if item.is_missing or not item.file_id:
raise AgentConfigServiceError(
"config_file_missing",
"config file must be uploaded before it can be used",
status_code=409,
)
return item
raise AgentConfigServiceError("config_file_not_found", "config file not found", status_code=404)
@staticmethod
def _available_skill_file_id(skill: AgentConfigSkillRefConfig) -> str:
if skill.is_missing or not skill.file_id:
raise AgentConfigServiceError(
"config_skill_missing",
"config skill must be uploaded before it can be used",
status_code=409,
)
return skill.file_id
@staticmethod
def _available_file_id(file_ref: AgentConfigFileRefConfig) -> str:
if file_ref.is_missing or not file_ref.file_id:
raise AgentConfigServiceError(
"config_file_missing",
"config file must be uploaded before it can be used",
status_code=409,
)
return file_ref.file_id
def _load_tool_file_bytes(self, *, tenant_id: str, file_id: str) -> tuple[bytes, str | None]:
with session_factory.create_session() as session:
tool_file = session.scalar(select(ToolFile).where(ToolFile.id == file_id, ToolFile.tenant_id == tenant_id))

View File

@ -22,7 +22,8 @@ from core.trigger.constants import (
from extensions.ext_redis import redis_client
from graphon.enums import BuiltinNodeTypes
from models import Account, App, AppMode
from models.agent import Agent, AgentConfigDraft, AgentConfigDraftType, AgentScope, AgentSource
from models.agent import Agent, AgentConfigDraft, AgentConfigDraftType, AgentConfigSnapshot, AgentScope, AgentSource
from models.agent_config_entities import AgentSoulConfig
from models.model import AppModelConfig, IconType
from services import app_dsl_service
from services.account_service import AccountService, TenantService
@ -969,6 +970,43 @@ class TestAppDslService:
account,
session=db_session_with_containers,
)
source_agent = db_session_with_containers.scalar(
select(Agent).where(
Agent.tenant_id == account.current_tenant_id,
Agent.app_id == source_app.id,
Agent.scope == AgentScope.ROSTER,
)
)
assert source_agent is not None
source_snapshot = db_session_with_containers.scalar(
select(AgentConfigSnapshot).where(
AgentConfigSnapshot.agent_id == source_agent.id,
AgentConfigSnapshot.id == source_agent.active_config_snapshot_id,
)
)
assert source_snapshot is not None
source_snapshot.config_snapshot = AgentSoulConfig.model_validate(
{
"config_skills": [
{
"name": "research",
"description": "Research source material.",
"file_id": "source-skill-file-id",
"size": 123,
}
],
"config_files": [
{
"name": "guide.md",
"file_kind": "upload_file",
"file_id": "source-config-file-id",
"size": 456,
"mime_type": "text/markdown",
}
],
}
)
db_session_with_containers.commit()
yaml_content = AppDslService.export_dsl(
source_app,
@ -979,13 +1017,42 @@ class TestAppDslService:
serialized_package = exported_data["agent_packages"][exported_data["agent"]["package_ref"]]
assert exported_data["app"]["mode"] == AppMode.AGENT.value
assert "agent_id" not in json.dumps(serialized_package)
assert serialized_package["soul"]["config_skills"] == [
{
"name": "research",
"description": "Research source material.",
"file_kind": "tool_file",
"file_id": "",
"is_missing": True,
"size": 123,
"hash": None,
"mime_type": "application/zip",
}
]
assert serialized_package["soul"]["config_files"] == [
{
"name": "guide.md",
"file_kind": "upload_file",
"file_id": "",
"is_missing": True,
"size": 456,
"hash": None,
"mime_type": "text/markdown",
}
]
assert "source-skill-file-id" not in yaml_content
assert "source-config-file-id" not in yaml_content
result = AppDslService(db_session_with_containers).import_app(
account=account,
import_mode=ImportMode.YAML_CONTENT,
yaml_content=yaml_content,
)
assert result.status == ImportStatus.COMPLETED
assert result.status == ImportStatus.COMPLETED_WITH_WARNINGS
assert {warning.code for warning in result.warnings} == {
"agent_file_omitted",
"agent_skill_omitted",
}
assert result.app_id is not None
db_session_with_containers.commit()
@ -1008,6 +1075,13 @@ class TestAppDslService:
)
assert draft is not None
assert draft.base_snapshot_id == imported_agent.active_config_snapshot_id
imported_soul = AgentSoulConfig.model_validate(draft.config_snapshot_dict)
assert imported_soul.config_skills[0].name == "research"
assert imported_soul.config_skills[0].file_id == ""
assert imported_soul.config_skills[0].is_missing is True
assert imported_soul.config_files[0].name == "guide.md"
assert imported_soul.config_files[0].file_id == ""
assert imported_soul.config_files[0].is_missing is True
def test_export_dsl_workflow_app_success(
self, db_session_with_containers: Session, mock_external_service_dependencies

View File

@ -291,6 +291,11 @@ def test_agent_app_list_and_create_use_agent_route(
]
},
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"load_reference_counts_by_agent_id",
lambda _self, **kwargs: {"agent-list": 2},
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"load_or_create_agent_app_debug_conversation_ids_by_agent_id",
@ -326,6 +331,7 @@ def test_agent_app_list_and_create_use_agent_route(
assert listed["data"][0]["debug_conversation_id"] == "debug-conversation-list"
assert listed["data"][0]["role"] == "List role"
assert listed["data"][0]["active_config_is_published"] is False
assert listed["data"][0]["reference_count"] == 2
assert listed["data"][0]["published_reference_count"] == 1
assert listed["data"][0]["published_references"] == [
{

View File

@ -1553,6 +1553,29 @@ def test_build_config_layer_config_missing_mentions_warn_without_catalog():
assert [w["code"] for w in warnings] == ["mention_target_missing"]
def test_build_config_layer_config_excludes_missing_assets_from_runtime():
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
soul = AgentSoulConfig.model_validate(
{
"prompt": {"system_prompt": "Use [§skill:missing-skill:Missing Skill§]."},
"config_skills": [{"name": "missing-skill", "file_id": "", "is_missing": True}],
"config_files": [{"name": "missing.txt", "file_kind": "upload_file", "file_id": "", "is_missing": True}],
}
)
config, warnings = build_config_layer_config(soul)
assert config.skills == []
assert config.files == []
assert config.mentioned_skill_names == []
assert [warning["code"] for warning in warnings] == [
"config_asset_missing",
"config_asset_missing",
"mention_target_missing",
]
# ── ENG-635: ask_human layer gating + feature manifest ───────────────────────

View File

@ -129,8 +129,12 @@ def test_make_portable_agent_package_strips_workspace_credentials_and_assets() -
assert package.soul.tools.dify_tools[0].credential_ref is None
assert package.soul.tools.dify_tools[0].runtime_parameters["upload_file_id"] is None
assert package.soul.tools.dify_tools[0].runtime_parameters["api_key"] is None
assert package.soul.config_skills == []
assert package.soul.config_files == []
assert package.soul.config_skills[0].name == "research"
assert package.soul.config_skills[0].file_id == ""
assert package.soul.config_skills[0].is_missing is True
assert package.soul.config_files[0].name == "guide.md"
assert package.soul.config_files[0].file_id == ""
assert package.soul.config_files[0].is_missing is True
assert [asset.kind for asset in package.omitted_assets] == ["skill", "file"]
assert "plain-secret" not in str(serialized)
assert "model-secret" not in str(serialized)
@ -530,7 +534,7 @@ def test_create_imported_roster_agent_app_prefixes_warnings(monkeypatch) -> None
assert app.name == "Portable Agent"
assert app.enable_site is True
assert app.enable_api is True
send.assert_called_once_with(app, account=SimpleNamespace(id="account-1"))
send.assert_called_once_with(app, account=SimpleNamespace(id="account-1"), session=session)
assert imported.warnings[0].path == "agent_packages.agent_1.soul.model"
@ -627,6 +631,25 @@ def test_resolve_package_soul_preserves_existing_and_marks_missing_knowledge(mon
assert datasets[0].id == "existing"
assert datasets[1].id is not None
assert datasets[1].id.startswith("missing-dataset-")
assert resolved.config_skills[0].model_dump(mode="json") == {
"name": "skill",
"description": "",
"file_kind": "tool_file",
"file_id": "",
"is_missing": True,
"size": None,
"hash": None,
"mime_type": "application/zip",
}
assert resolved.config_files[0].model_dump(mode="json") == {
"name": "Guide",
"file_kind": "upload_file",
"file_id": "",
"is_missing": True,
"size": None,
"hash": None,
"mime_type": None,
}
assert {warning.code for warning in warnings} == {
"agent_skill_omitted",
"agent_file_omitted",

View File

@ -2284,6 +2284,7 @@ def test_roster_list_and_invite_options(monkeypatch: pytest.MonkeyPatch):
lambda version_ids: {"version-1": version, "version-2": unconfigured_version},
)
monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {})
monkeypatch.setattr(service, "_load_reference_counts_by_agent_id", lambda **kwargs: {"agent-1": 1})
listed = service.list_roster_agents(tenant_id="tenant-1", page=1, limit=20)
invited = service.list_invite_options(tenant_id="tenant-1", page=1, limit=20, app_id="app-1")
@ -2297,6 +2298,7 @@ def test_roster_list_and_invite_options(monkeypatch: pytest.MonkeyPatch):
assert listed["data"][0]["updated_at"] == int(updated_at.timestamp())
assert listed["data"][0]["active_config_snapshot"]["created_at"] == int(version_created_at.timestamp())
assert listed["data"][0]["active_config_is_published"] is True
assert listed["data"][0]["reference_count"] == 1
assert listed["data"][1]["active_config_is_published"] is False
assert invited["data"][0]["is_in_current_workflow"] is True
assert invited["data"][0]["existing_node_ids"] == ["node-1"]
@ -2327,6 +2329,7 @@ def test_invite_options_uses_db_filtered_pagination(monkeypatch: pytest.MonkeyPa
},
)
monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {})
monkeypatch.setattr(service, "_load_reference_counts_by_agent_id", lambda **kwargs: {})
result = service.list_invite_options(tenant_id="tenant-1", page=1, limit=1)
@ -2498,6 +2501,43 @@ def test_published_references_include_app_display_fields_and_sort_by_updated_at(
assert references[0]["workflow_version"] == "published-recent"
def test_reference_counts_include_draft_and_published_bindings_once_per_app():
bindings = [
SimpleNamespace(
agent_id="agent-1",
app_id="app-1",
workflow_id="workflow-draft",
workflow_version=Workflow.VERSION_DRAFT,
),
SimpleNamespace(
agent_id="agent-1",
app_id="app-1",
workflow_id="workflow-published",
workflow_version="v1",
),
SimpleNamespace(
agent_id="agent-1",
app_id="app-2",
workflow_id="workflow-stale",
workflow_version="old-version",
),
]
apps = [
SimpleNamespace(id="app-1", workflow_id="workflow-published"),
SimpleNamespace(id="app-2", workflow_id="workflow-stale"),
]
workflows = [
SimpleNamespace(id="workflow-draft", app_id="app-1", version=Workflow.VERSION_DRAFT),
SimpleNamespace(id="workflow-published", app_id="app-1", version="v1"),
SimpleNamespace(id="workflow-stale", app_id="app-2", version="current-version"),
]
service = AgentRosterService(FakeSession(scalars=[bindings, apps, workflows]))
result = service._load_reference_counts_by_agent_id(tenant_id="tenant-1", agent_ids=["agent-1"])
assert result == {"agent-1": 1}
def test_roster_update_archive_versions_and_detail(monkeypatch: pytest.MonkeyPatch):
listed_version = AgentConfigSnapshot(id="version-4", agent_id="agent-1", version=4)
listed_version_created_at = datetime(2026, 1, 5, 3, 4, 5, tzinfo=UTC)
@ -3933,6 +3973,28 @@ class TestWorkflowAgentDraftBindingSync:
draft_workflow=self._agent_workflow(),
)
def test_publish_validation_rejects_missing_config_assets(self):
payload = ComposerSavePayload.model_validate(
{
"variant": "agent_app",
"save_strategy": "save_as_new_version",
"agent_soul": {
"config_skills": [{"name": "research", "file_id": "", "is_missing": True}],
"config_files": [
{
"name": "guide.txt",
"file_kind": "upload_file",
"file_id": "",
"is_missing": True,
}
],
},
}
)
with pytest.raises(InvalidComposerConfigError, match="config_asset_missing.*skill:research.*file:guide.txt"):
ComposerConfigValidator.validate_publish_payload(payload)
def test_projects_binding_declared_outputs_to_draft_graph_response(self):
workflow = Workflow(
id="workflow-1",

View File

@ -13,6 +13,7 @@ from models.agent_config_entities import (
AgentConfigFileRefConfig,
AgentConfigSkillRefConfig,
AgentEnvVariableConfig,
AgentFileRefConfig,
AgentSoulConfig,
)
from services.agent.skill_package_service import SkillPackageError
@ -211,7 +212,14 @@ def test_push_for_console_allows_shared_draft_mutations() -> None:
def test_push_accepts_tenant_scoped_tool_file_sources_from_different_upload_owner() -> None:
session = MagicMock()
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.BUILD_DRAFT, writable=True)
target = _target(
kind=AgentConfigVersionKind.BUILD_DRAFT,
writable=True,
soul=_soul(
config_skills=[{"name": "alpha", "file_id": "", "is_missing": True}],
config_files=[{"name": "guide.txt", "file_kind": "tool_file", "file_id": "", "is_missing": True}],
),
)
file_source = SimpleNamespace(
id="tool-file-file",
tenant_id=TENANT,
@ -269,7 +277,9 @@ def test_push_accepts_tenant_scoped_tool_file_sources_from_different_upload_owne
assert isinstance(files, dict)
assert isinstance(skills, dict)
assert files["items"][0]["file_id"] == "tool-file-file"
assert files["items"][0]["is_missing"] is False
assert skills["items"][0]["file_id"] == "normalized-skill-file"
assert skills["items"][0]["is_missing"] is False
session.commit.assert_called_once()
@ -301,7 +311,11 @@ def test_push_file_for_console_rejects_snapshot_writes() -> None:
def test_push_file_for_console_uses_service_owned_upload_lookup_and_naming() -> None:
session = MagicMock()
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False)
target = _target(
kind=AgentConfigVersionKind.DRAFT,
writable=False,
soul=_soul(config_files=[{"name": "guide.txt", "file_kind": "upload_file", "file_id": "", "is_missing": True}]),
)
upload_file = SimpleNamespace(
id="upload-1",
name="guide.txt",
@ -329,6 +343,7 @@ def test_push_file_for_console_uses_service_owned_upload_lookup_and_naming() ->
"id": "guide.txt",
"name": "guide.txt",
"file_id": "upload-1",
"is_missing": False,
"size": 7,
"hash": "sha256:abc",
"mime_type": "text/plain",
@ -508,6 +523,7 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
"id": "alpha",
"name": "alpha",
"file_id": "tool-file-1",
"is_missing": False,
"description": "Alpha skill",
"size": None,
"hash": None,
@ -521,6 +537,7 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
"id": "guide.txt",
"name": "guide.txt",
"file_id": "upload-file-1",
"is_missing": False,
"size": None,
"hash": None,
"mime_type": None,
@ -532,6 +549,63 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
}
def test_manifest_preserves_missing_config_assets_and_pull_rejects_them() -> None:
soul = _soul(
config_skills=[{"name": "alpha", "file_id": "", "is_missing": True}],
config_files=[{"name": "guide.txt", "file_kind": "upload_file", "file_id": "", "is_missing": True}],
)
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=soul)
service = AgentConfigService()
manifest = service._manifest_for_target(target)
assert manifest["skills"]["items"][0]["is_missing"] is True # type: ignore[index]
assert manifest["files"]["items"][0]["is_missing"] is True # type: ignore[index]
with patch.object(service, "resolve_target", return_value=target):
with pytest.raises(AgentConfigServiceError) as skill_error:
service.pull_skill(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="alpha",
user_id=USER,
)
with pytest.raises(AgentConfigServiceError) as file_error:
service.pull_file(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="guide.txt",
user_id=USER,
)
assert (skill_error.value.code, skill_error.value.status_code) == ("config_skill_missing", 409)
assert (file_error.value.code, file_error.value.status_code) == ("config_file_missing", 409)
def test_config_asset_refs_require_file_id_unless_marked_missing() -> None:
assert AgentFileRefConfig().file_id is None
assert (
AgentConfigFileRefConfig(
name="guide.txt",
file_kind="upload_file",
is_missing=True,
).file_id
== ""
)
with pytest.raises(ValueError, match="file_id is required"):
AgentConfigSkillRefConfig(name="alpha")
with pytest.raises(ValueError, match="must not retain"):
AgentConfigFileRefConfig(
name="guide.txt",
file_kind="upload_file",
file_id="workspace-file-id",
is_missing=True,
)
def test_preview_skill_file_returns_text_preview() -> None:
service = AgentConfigService()
target = _target(

View File

@ -67,23 +67,23 @@ make test
Each agent job runs inside a Landlock sandbox that restricts filesystem access:
| Access | Paths (defaults) |
|--------|------------------|
| **Read-Write** | `$HOME` (always, includes `$CWD/.tmp` as `TMPDIR`) |
| **Read-Write (dev)** | `/dev/null`, `/dev/zero`, `/dev/urandom`, `/dev/random`, `/dev/tty` |
| Access | Paths (defaults) |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Read-Write** | `$HOME` (always, includes `$CWD/.tmp` as `TMPDIR`) |
| **Read-Write (dev)** | `/dev/null`, `/dev/zero`, `/dev/urandom`, `/dev/random`, `/dev/tty` |
| **Read-Only + Exec** | `/usr`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/etc`, `/proc`, `/opt/dify-agent-tools`, `/opt/homebrew`, `/snap` |
| **Denied** | Everything else (`/tmp`, other agents' homes, `/var`, `/srv`, etc.) |
| **Denied** | Everything else (`/tmp`, other agents' homes, `/var`, `/srv`, etc.) |
The runner automatically creates `$CWD/.tmp` and sets `TMPDIR`, `TMP`, `TEMP` to it, so temp files stay isolated per workspace.
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_PATH_ISOLATION` | `true` | Set to `false` to disable Landlock entirely |
| `LANDLOCK_RW_PATHS` | *(empty)* | Comma-separated RW directories (besides `$HOME`) |
| `LANDLOCK_RO_PATHS` | `/usr,/bin,...` | Comma-separated RO+exec directories |
| `LANDLOCK_RW_DEV_PATHS` | `/dev/null,...` | Comma-separated device files with RW access |
| Variable | Default | Description |
| ----------------------- | --------------- | ------------------------------------------------ |
| `ENABLE_PATH_ISOLATION` | `true` | Set to `false` to disable Landlock entirely |
| `LANDLOCK_RW_PATHS` | _(empty)_ | Comma-separated RW directories (besides `$HOME`) |
| `LANDLOCK_RO_PATHS` | `/usr,/bin,...` | Comma-separated RO+exec directories |
| `LANDLOCK_RW_DEV_PATHS` | `/dev/null,...` | Comma-separated device files with RW access |
Requires Linux ≥ 5.13. On unsupported kernels, a warning is printed to stderr.

View File

@ -519,6 +519,7 @@ export type AgentAppPartial = {
permission_keys?: Array<string>
published_reference_count?: number
published_references?: Array<AgentAppPublishedReferenceResponse>
reference_count?: number | null
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
@ -610,6 +611,7 @@ export type AgentInviteOptionResponse = {
published_node_reference_count?: number
published_reference_count?: number
published_references?: Array<AgentPublishedReferenceResponse>
reference_count?: number | null
role?: string
scope: AgentScope
source: AgentSource
@ -757,6 +759,7 @@ export type AgentConfigFileItemResponse = {
file_id?: string | null
hash?: string | null
id: string
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -775,6 +778,7 @@ export type AgentConfigSkillItemResponse = {
file_id?: string | null
hash?: string | null
id: string
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -1134,9 +1138,10 @@ export type AppVariableConfig = {
}
export type AgentConfigFileRefConfig = {
file_id: string
file_id?: string
file_kind: 'tool_file' | 'upload_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -1144,9 +1149,10 @@ export type AgentConfigFileRefConfig = {
export type AgentConfigSkillRefConfig = {
description?: string
file_id: string
file_id?: string
file_kind?: 'tool_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -1919,6 +1925,7 @@ export type AgentAppPartialWritable = {
permission_keys?: Array<string>
published_reference_count?: number
published_references?: Array<AgentAppPublishedReferenceResponse>
reference_count?: number | null
role?: string | null
tags?: Array<Tag>
updated_at?: number | null

View File

@ -461,6 +461,7 @@ export const zAgentConfigFileItemResponse = z.object({
file_id: z.string().nullish(),
hash: z.string().nullish(),
id: z.string(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string(),
size: z.int().nullish(),
@ -498,6 +499,7 @@ export const zAgentConfigSkillItemResponse = z.object({
file_id: z.string().nullish(),
hash: z.string().nullish(),
id: z.string(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string(),
size: z.int().nullish(),
@ -983,6 +985,7 @@ export const zAgentAppPartial = z.object({
permission_keys: z.array(z.string()).optional(),
published_reference_count: z.int().optional().default(0),
published_references: z.array(zAgentAppPublishedReferenceResponse).optional(),
reference_count: z.int().nullish(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
@ -1134,6 +1137,7 @@ export const zAgentInviteOptionResponse = z.object({
published_node_reference_count: z.int().optional().default(0),
published_reference_count: z.int().optional().default(0),
published_references: z.array(zAgentPublishedReferenceResponse).optional(),
reference_count: z.int().nullish(),
role: z.string().optional().default(''),
scope: zAgentScope,
source: zAgentSource,
@ -1191,9 +1195,10 @@ export const zAppVariableConfig = z.object({
* Stable Agent Soul reference to one config file payload.
*/
export const zAgentConfigFileRefConfig = z.object({
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.enum(['tool_file', 'upload_file']),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string().min(1).max(255),
size: z.int().nullish(),
@ -1206,9 +1211,10 @@ export const zAgentConfigFileRefConfig = z.object({
*/
export const zAgentConfigSkillRefConfig = z.object({
description: z.string().optional().default(''),
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.literal('tool_file').optional().default('tool_file'),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish().default('application/zip'),
name: z.string().min(1).max(255),
size: z.int().nullish(),
@ -2634,6 +2640,7 @@ export const zAgentAppPartialWritable = z.object({
permission_keys: z.array(z.string()).optional(),
published_reference_count: z.int().optional().default(0),
published_references: z.array(zAgentAppPublishedReferenceResponse).optional(),
reference_count: z.int().nullish(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),

View File

@ -1431,6 +1431,7 @@ export type AgentConfigFileItemResponse = {
file_id?: string | null
hash?: string | null
id: string
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -1449,6 +1450,7 @@ export type AgentConfigSkillItemResponse = {
file_id?: string | null
hash?: string | null
id: string
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -2396,9 +2398,10 @@ export type AppVariableConfig = {
}
export type AgentConfigFileRefConfig = {
file_id: string
file_id?: string
file_kind: 'tool_file' | 'upload_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -2406,9 +2409,10 @@ export type AgentConfigFileRefConfig = {
export type AgentConfigSkillRefConfig = {
description?: string
file_id: string
file_id?: string
file_kind?: 'tool_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null

View File

@ -1014,6 +1014,7 @@ export const zAgentConfigFileItemResponse = z.object({
file_id: z.string().nullish(),
hash: z.string().nullish(),
id: z.string(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string(),
size: z.int().nullish(),
@ -1051,6 +1052,7 @@ export const zAgentConfigSkillItemResponse = z.object({
file_id: z.string().nullish(),
hash: z.string().nullish(),
id: z.string(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string(),
size: z.int().nullish(),
@ -2832,9 +2834,10 @@ export const zAppVariableConfig = z.object({
* Stable Agent Soul reference to one config file payload.
*/
export const zAgentConfigFileRefConfig = z.object({
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.enum(['tool_file', 'upload_file']),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string().min(1).max(255),
size: z.int().nullish(),
@ -2847,9 +2850,10 @@ export const zAgentConfigFileRefConfig = z.object({
*/
export const zAgentConfigSkillRefConfig = z.object({
description: z.string().optional().default(''),
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.literal('tool_file').optional().default('tool_file'),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish().default('application/zip'),
name: z.string().min(1).max(255),
size: z.int().nullish(),

View File

@ -574,9 +574,10 @@ export type AppVariableConfig = {
}
export type AgentConfigFileRefConfig = {
file_id: string
file_id?: string
file_kind: 'tool_file' | 'upload_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null
@ -584,9 +585,10 @@ export type AgentConfigFileRefConfig = {
export type AgentConfigSkillRefConfig = {
description?: string
file_id: string
file_id?: string
file_kind?: 'tool_file'
hash?: string | null
is_missing?: boolean
mime_type?: string | null
name: string
size?: number | null

View File

@ -568,9 +568,10 @@ export const zAppVariableConfig = z.object({
* Stable Agent Soul reference to one config file payload.
*/
export const zAgentConfigFileRefConfig = z.object({
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.enum(['tool_file', 'upload_file']),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish(),
name: z.string().min(1).max(255),
size: z.int().nullish(),
@ -583,9 +584,10 @@ export const zAgentConfigFileRefConfig = z.object({
*/
export const zAgentConfigSkillRefConfig = z.object({
description: z.string().optional().default(''),
file_id: z.string().min(1).max(255),
file_id: z.string().max(255).optional().default(''),
file_kind: z.literal('tool_file').optional().default('tool_file'),
hash: z.string().nullish(),
is_missing: z.boolean().optional().default(false),
mime_type: z.string().nullish().default('application/zip'),
name: z.string().min(1).max(255),
size: z.int().nullish(),