diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12491248..7be6216c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2652,6 +2652,50 @@ or filesystem isolation. Automatic installation remains separate. four platforms. Companion installation does not grant API authorization; task-scoped uploads keep the current run requester and workspace checks. +### Product Agent resource configuration + +The V1 product definition lives in the [Agent configuration product document](https://vrfi1sk8a0.feishu.cn/wiki/TEhDwRlRpiGIcRkbKywcfEydn7g). +The Build inventory is Models, Environments, Capabilities, Integrations and +Credentials. Agent creation/editing selects those resources plus Harness. Product +resource selection and credential binding belong to Parsar; public protocol +features not yet implemented by Core remain explicit reservations, not alternate +product execution paths or changes to Core's contract. + +- `resource_bindings` on Agent create/update contains version, pinning mode and + per-resource configuration. Agent changes and the complete binding selection + commit atomically. Omitting the field preserves bindings; an empty array clears + them. Updating credentials preserves the selected version and pinning mode. + Retaining an existing binding does not reapply new-install eligibility after + unpublishing or deprecation; changing its version or tracking mode does. Retained + foreign private resources never expose later private revisions. +- Capability bindings reuse `agent_capabilities` and Build imports. Model + credentials default to the Provider key; an explicit + `config.model_credential_binding` chooses a credential `kind` and personal/shared + source. Model bindings accept only `openai_api_key` and `anthropic_api_key`; + unrelated personal secrets must never become Provider tokens. Migrations register + these model credential kinds without requiring development fixtures. Personal means + the run requester, never the creator of a + shared Agent. Public Agents require shared credentials. Existing per-capability + choices take precedence over Agent-wide defaults. Missing references remain + visible and require repair; lookup failures never substitute another credential. +- Inline resource creation keeps the Agent draft. Workspace communication + connectors (Feishu, Slack, Discord and Teams) retain their shared scope and + existing management APIs, with immediate-save scope stated in the Agent form. + Dedicated Agent Feishu configuration remains owned by its existing API and is + preserved when execution configuration is replaced. +- Skills verify the original owned archive and checksum, then adapt Build rootless + ZIP layout and descriptive slug/title metadata to public inline Skill initialization. + Supporting bytes and executable modes are preserved; native activation controls + remain unsupported. System prompts and knowledge + become protocol instructions. Core currently cannot combine a referenced + Environment Template with additional Skills, or hosted execution with MCP; + Plugins also remain unsupported. Preserve the Template selector and show these + gaps. Do not silently drop a binding to start a partially configured Agent. +- Freeze Skill bytes and selected model credentials in the existing encrypted + private Session snapshot, separate from the ordinary request. Restore the + existing snapshot before reading mutable resources; decrypt initialization only + for the Core request. Editing resources or credentials affects new Sessions. + ### Product model catalog and execution credentials Parsar owns workspace Model Providers, their write-only API keys, model catalog, @@ -2793,7 +2837,7 @@ Sessions only. Keep the existing product navigation and direct empty-chat compos Auth, workspace membership, conversation ownership, IM, MCP access to Agents, scheduling, auditing and billing remain product concerns. - The product currently supports text input, assistant text, reasoning summaries, - tool observations, raw token usage and cancellation. Capability bindings, + tool observations, raw token usage and cancellation. Unsupported capability profiles, attachments, interactive approvals, application function results, local-device and sandbox administration are unavailable in this product client. Show this limitation in the Agent configuration page; reject unsupported input rather than silently @@ -2807,7 +2851,8 @@ Sessions only. Keep the existing product navigation and direct empty-chat compos import/upload, MCP configuration/OAuth, permissions and business orchestration. These are product asset operations, not runtime installation. Browser plugin extensions also remain product UI behavior. Runtime capability activation and - loading must use Core; unsupported execution binding mutations are not exposed. + loading must use Core; reserved unsupported bindings must display their execution + limitation and fail before Session creation, never silently disappear. Asset import or OAuth success must never imply readiness for Agent execution. Landed migrations and retained business history are not rewritten or deleted. - Soft-deleting an Agent preserves authorized conversation/run history and its diff --git a/apps/web/src/i18n/locales/en-US/admin.json b/apps/web/src/i18n/locales/en-US/admin.json index 602c8348..3b37e6ec 100644 --- a/apps/web/src/i18n/locales/en-US/admin.json +++ b/apps/web/src/i18n/locales/en-US/admin.json @@ -2955,5 +2955,41 @@ "deleteTitle": "Delete {{name}}?", "deleteHint": "Existing Sessions keep their frozen configuration. Agents using this model or Provider cannot start new Sessions until another model is selected.", "incompatible": "Choose a compatible Harness and a hosted environment for this model." + }, + "agentResources": { + "title": "Skills and tool integrations", + "hint": "Select Build resources and configure each version and credential. Environment Template keeps its own configuration.", + "select": "Select an existing resource", + "add": "Add resource", + "remove": "Remove", + "unsupported": "Not supported by this runtime yet", + "version": "Version for {{name}}", + "latest": "Follow latest version", + "loading": "Loading…", + "loadFailed": "Could not load. Please retry.", + "retry": "Retry", + "personal": "Use each caller’s personal credential", + "shared": "Shared credential", + "unavailable": "Saved resource unavailable — choose a replacement", + "sharedReady": "Shared credential configured", + "personalReady": "Configured for you; other callers need their own credential", + "missingCredential": "Credential missing. Select or add one.", + "addPersonal": "Add my credential", + "addShared": "Add shared credential", + "credentialName": "Credential name", + "credentialValue": "Credential value", + "connections": "Communication integrations", + "workspaceScope": "These connections are shared by the workspace. Changes immediately affect all its Agents. Close configuration to continue this Agent draft.", + "enabled": "Enabled", + "notEnabled": "Not enabled", + "configure": "Configure", + "modelCredential": "Model credential", + "providerCredential": "Use the Provider’s configured key", + "skillCredentialsPending": "Configuration will be saved. Skills that declare credentials cannot run until Skill credential delivery is supported.", + "mcpPending": "Configuration will be saved. Hosted MCP awaits Core support; an Agent with this binding cannot run yet.", + "templateSkillsPending": "Template with additional Skills awaits Core support. You can save this configuration; use a hosted Sandbox without a template to run now.", + "dedicatedFeishu": "Configure this Agent’s dedicated Feishu bot", + "dedicatedAfterCreate": "After creating the Agent, configure its dedicated Feishu bot in the editor.", + "sandbox": "Sandbox" } } diff --git a/apps/web/src/i18n/locales/zh-CN/admin.json b/apps/web/src/i18n/locales/zh-CN/admin.json index d11ea3dd..e3592961 100644 --- a/apps/web/src/i18n/locales/zh-CN/admin.json +++ b/apps/web/src/i18n/locales/zh-CN/admin.json @@ -2955,5 +2955,41 @@ "deleteTitle": "删除 {{name}}?", "deleteHint": "已有 Session 保留原配置。使用此模型或 Provider 的 Agent 需要选择其他模型后才能启动新 Session。", "incompatible": "请为此模型选择兼容的 Harness 和托管环境。" + }, + "agentResources": { + "title": "Skills 与工具集成", + "hint": "选择 Build 中的资源,并分别配置版本和凭据。Environment Template 保留自己的配置。", + "select": "选择已有资源", + "add": "新增资源", + "remove": "移除", + "unsupported": "当前运行环境暂不支持", + "version": "{{name}} 的版本", + "latest": "跟随最新版本", + "loading": "正在加载…", + "loadFailed": "加载失败,请重试", + "retry": "重试", + "personal": "使用各调用者的个人凭据", + "shared": "共享凭据", + "unavailable": "已保存的资源不可用,请重新选择", + "sharedReady": "共享凭据已配置", + "personalReady": "当前用户已配置;其他调用者需要自己的凭据", + "missingCredential": "缺少可用凭据,请选择或补充", + "addPersonal": "补充我的凭据", + "addShared": "新增共享凭据", + "credentialName": "凭据名称", + "credentialValue": "凭据内容", + "connections": "通讯集成", + "workspaceScope": "以下连接由工作区共享,修改会立即影响此工作区的所有 Agent。关闭配置后可继续填写当前 Agent。", + "enabled": "已启用", + "notEnabled": "未启用", + "configure": "配置", + "modelCredential": "模型凭据", + "providerCredential": "使用 Provider 配置的 Key", + "skillCredentialsPending": "配置可以保存。声明了凭据的 Skill 暂时无法运行,需等待 Skill 凭据注入支持。", + "mcpPending": "配置将保存。托管 Sandbox 的 MCP 调用等待 Core 支持;绑定此资源的 Agent 当前不能运行。", + "templateSkillsPending": "Template 与额外 Skills 的组合等待 Core 支持。配置可保存;当前运行请使用不带 Template 的托管 Sandbox。", + "dedicatedFeishu": "配置此 Agent 的专用飞书机器人", + "dedicatedAfterCreate": "创建 Agent 后,可在编辑页配置专用飞书机器人。", + "sandbox": "Sandbox" } } diff --git a/apps/web/src/lib/api-types.ts b/apps/web/src/lib/api-types.ts index 588b93e7..f01cda8b 100644 --- a/apps/web/src/lib/api-types.ts +++ b/apps/web/src/lib/api-types.ts @@ -206,6 +206,7 @@ export interface AgentInlineNewSecret { } export interface CreateAgentRequest { + resource_bindings?: InitialAgentCapabilityRequest[] name: string slug?: string description?: string @@ -216,6 +217,7 @@ export interface CreateAgentRequest { } export interface UpdateAgentRequest { + resource_bindings?: InitialAgentCapabilityRequest[] name?: string description?: string connector_type?: "agents_api" @@ -895,6 +897,7 @@ export interface ListSecretsResponse { } export interface CreateSecretRequest { + credential_kind_code?: string name: string /** Defaults to `model_provider` server-side when omitted. */ kind?: string diff --git a/apps/web/src/pages/admin/CreateAgentDialog.tsx b/apps/web/src/pages/admin/CreateAgentDialog.tsx index 06e306ad..e5d7465d 100644 --- a/apps/web/src/pages/admin/CreateAgentDialog.tsx +++ b/apps/web/src/pages/admin/CreateAgentDialog.tsx @@ -1,3 +1,10 @@ +import { useAgentCloneCredentials } from "./agents/useAgentCloneCredentials" +import { AgentCloneCredentials } from "./agents/AgentCloneCredentials" +import { useSecrets } from "../../lib/api-secrets" +import { AgentResourceFields } from "./agents/AgentResourceFields" +import { AgentConnectionsField } from "./agents/AgentConnectionsField" +import { AgentModelCredentialField, type AgentModelCredential } from "./agents/AgentModelCredentialField" +import { useAgentResourceDraft } from "./agents/useAgentResourceDraft" import { Textarea } from "../../components/ui/textarea" import { jsonObject, coreExecutionDefaults, type CoreAgentConfig } from "../../lib/core-api" import { useId, useRef, useState } from "react" @@ -43,16 +50,28 @@ export function CreateAgentDialog(props: CreateAgentDialogProps) { const [name, setName] = useState(props.agent?.name ?? "") const [description, setDescription] = useState(props.agent?.description ?? "") const [modelID, setModelID] = useState(String(props.agent?.config?.model_id ?? "")) + const isClone = props.mode === "create" && !!props.agent + const resources = useAgentResourceDraft(props.workspaceID, props.agent?.id) + const cloneSecrets = useSecrets(isClone ? props.workspaceID : null) + const cloneCredentials = useAgentCloneCredentials(isClone ? props.agent!.id : null, props.workspaceID, props.agent?.config ?? {}, resources.bindings, + resources.selection?.map(item => item.capability) ?? [], resources.selection?.map(item => item.capability.id) ?? [], + Object.fromEntries((resources.selection ?? []).map(item => [item.capability.id, { pinningMode: item.pinning_mode === "latest" ? "latest" : "pinned", versionID: item.capability_version_id }])), cloneSecrets.data?.secrets ?? []) + const [modelCredential, setModelCredential] = useState(() => { + const saved = props.agent?.config?.model_credential_binding as AgentModelCredential | undefined + return isClone && saved?.source === "personal" ? { kind: saved.kind, source: "shared" } : saved + }) const catalog = useModelCatalog(props.workspaceID) const model = catalog.data?.models.find(row => row.id === modelID) const [instructions, setInstructions] = useState(String(props.agent?.config?.system_prompt ?? "")) const [advanced, setAdvanced] = useState(() => JSON.stringify(Object.fromEntries(Object.entries(props.agent?.config ?? {}).filter(([key]) => ["tools", "service_tier", "multi_agent", "reasoning", "text"].includes(key))), null, 2)) const [configurationError, setConfigurationError] = useState(null) const [visibility, setVisibility] = useState(props.agent?.visibility ?? "workspace") - const valid = name.trim() !== "" && !!model && supportsCatalogModel(model, execution.harness) && execution.environment.type === "openai_hosted" && execution.harness !== "" && props.workspaceID !== null + const cloneReady = !isClone || (cloneCredentials.ready && cloneCredentials.valid && !cloneSecrets.error && !cloneSecrets.isLoading && (!modelCredential || (modelCredential.source === "shared" && !!modelCredential.secret_id))) + const valid = cloneReady && !resources.loading && !resources.error && name.trim() !== "" && !!model && supportsCatalogModel(model, execution.harness) && execution.environment.type === "openai_hosted" && execution.harness !== "" && props.workspaceID !== null return
{ + if (event.target !== event.currentTarget) return event.preventDefault() if (!valid || props.pending) return let extra: Record @@ -63,7 +82,7 @@ export function CreateAgentDialog(props: CreateAgentDialogProps) { } catch (error) { setConfigurationError(error instanceof Error ? error.message : t("core.failed")); return } props.onSubmit({ agentID: props.mode === "edit" ? props.agent?.id : undefined, - body: { name: name.trim(), description: description.trim(), connector_type: "agents_api", system_prompt: instructions, config: { ...extra, model: model!.model_key, model_id: modelID, environment: execution.environment, x_agents_core: { harness: execution.harness } } as CoreAgentConfig, ...(props.mode === "create" ? { visibility } : {}) }, + body: { resource_bindings: resources.selection?.map(({ capability, ...binding }) => ({ ...binding, ...(isClone ? { configuration: cloneCredentials.rows.find(row => row.capabilityID === capability.id)?.configuration ?? {} } : {}) })), name: name.trim(), description: description.trim(), connector_type: "agents_api", system_prompt: instructions, config: { ...extra, ...(!isClone && props.agent?.config?.credential_bindings ? { credential_bindings: props.agent.config.credential_bindings } : {}), ...(modelCredential ? { model_credential_binding: modelCredential } : {}), model: model!.model_key, model_id: modelID, environment: execution.environment, x_agents_core: { harness: execution.harness } } as CoreAgentConfig, ...(props.mode === "create" ? { visibility } : {}) }, }) }}> @@ -74,8 +93,14 @@ export function CreateAgentDialog(props: CreateAgentDialogProps) {
setName(event.target.value)} disabled={props.pending} required />
setDescription(event.target.value)} disabled={props.pending} />
+ setExecution(value => ({ ...value, harness }))} onEnvironmentChange={environment => setExecution(value => ({ ...value, environment }))} disabled={props.pending} /> {model && (!supportsCatalogModel(model, execution.harness) || execution.environment.type !== "openai_hosted") &&

{t("catalog.incompatible")}

} + {resources.selection?.some(item => item.capability.type === "skill") && execution.environment.type === "openai_hosted" && execution.environment.environment_template_id &&

{t("agentResources.templateSkillsPending")}

} + {isClone &&

{t("agents.form.clone.credentials")}

} + + {isClone && { void cloneSecrets.refetch(); cloneCredentials.retry() }} />} + {props.mode === "create" && }
{t("core.advanced")}

{t("core.agentAdvancedHint")}