diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index c8d2134ed83..c4acb501cc3 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -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"] = [ diff --git a/api/controllers/console/app/agent_config_inspector.py b/api/controllers/console/app/agent_config_inspector.py index 170b3756b39..6fd28bcbb29 100644 --- a/api/controllers/console/app/agent_config_inspector.py +++ b/api/controllers/console/app/agent_config_inspector.py @@ -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 diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index 3afa740e545..22b99412053 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -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, diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index db0dbf8dcc0..50105273e1c 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -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) diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py index a78f7d90dd5..7ce6a103046 100644 --- a/api/models/agent_config_entities.py +++ b/api/models/agent_config_entities.py @@ -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") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 19ed007db00..578a5b0be07 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -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,
**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,
**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 | diff --git a/api/services/agent/composer_validator.py b/api/services/agent/composer_validator.py index a227978b764..ea1ae673b81 100644 --- a/api/services/agent/composer_validator.py +++ b/api/services/agent/composer_validator.py @@ -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. diff --git a/api/services/agent/dsl_entities.py b/api/services/agent/dsl_entities.py index 3c00b2a7f5a..70dbf3a2967 100644 --- a/api/services/agent/dsl_entities.py +++ b/api/services/agent/dsl_entities.py @@ -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 diff --git a/api/services/agent/dsl_service.py b/api/services/agent/dsl_service.py index 70e7d474f98..0200a22704f 100644 --- a/api/services/agent/dsl_service.py +++ b/api/services/agent/dsl_service.py @@ -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, diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index d1b093a5a65..c6087843ae0 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -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 {} diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index 0b4c6b59cbd..0997cfd2029 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -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}: diff --git a/api/services/agent_config_service.py b/api/services/agent_config_service.py index a0da08e5c2e..ded7570eb0f 100644 --- a/api/services/agent_config_service.py +++ b/api/services/agent_config_service.py @@ -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)) diff --git a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py index 6a9d7979931..224c08384ad 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py @@ -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 diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index e25f5bea5bf..b026c0e0c85 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -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"] == [ { diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index 9d0089e8e08..811620e4005 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -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 ─────────────────────── diff --git a/api/tests/unit_tests/services/agent/test_agent_dsl_service.py b/api/tests/unit_tests/services/agent/test_agent_dsl_service.py index 926a04f53f9..00aecefbaa6 100644 --- a/api/tests/unit_tests/services/agent/test_agent_dsl_service.py +++ b/api/tests/unit_tests/services/agent/test_agent_dsl_service.py @@ -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", diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index e19fa36dca9..eb8f87fc1a9 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -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", diff --git a/api/tests/unit_tests/services/test_agent_config_service.py b/api/tests/unit_tests/services/test_agent_config_service.py index f9a649a13ab..4b1313be7c6 100644 --- a/api/tests/unit_tests/services/test_agent_config_service.py +++ b/api/tests/unit_tests/services/test_agent_config_service.py @@ -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( diff --git a/dify-agent-runtime/README.md b/dify-agent-runtime/README.md index 74e796a1d5c..7abc105211f 100644 --- a/dify-agent-runtime/README.md +++ b/dify-agent-runtime/README.md @@ -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. diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index ec29f5662b0..794f5687d84 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -519,6 +519,7 @@ export type AgentAppPartial = { permission_keys?: Array published_reference_count?: number published_references?: Array + reference_count?: number | null role?: string | null tags?: Array updated_at?: number | null @@ -610,6 +611,7 @@ export type AgentInviteOptionResponse = { published_node_reference_count?: number published_reference_count?: number published_references?: Array + 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 published_reference_count?: number published_references?: Array + reference_count?: number | null role?: string | null tags?: Array updated_at?: number | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 0687f741c6d..a7d905f1fb4 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -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(), diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index afc4c5edf03..404b3afe0b6 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -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 diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 5625ee8c10e..97b9e281afd 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -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(), diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index 8fb46ea24ba..4776fda89f5 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -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 diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 06b8dac4e6f..d0b1a2b6169 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -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(),