Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/i18n/locales/en-US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
36 changes: 36 additions & 0 deletions apps/web/src/i18n/locales/zh-CN/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 3 additions & 0 deletions apps/web/src/lib/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export interface AgentInlineNewSecret {
}

export interface CreateAgentRequest {
resource_bindings?: InitialAgentCapabilityRequest[]
name: string
slug?: string
description?: string
Expand All @@ -216,6 +217,7 @@ export interface CreateAgentRequest {
}

export interface UpdateAgentRequest {
resource_bindings?: InitialAgentCapabilityRequest[]
name?: string
description?: string
connector_type?: "agents_api"
Expand Down Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions apps/web/src/pages/admin/CreateAgentDialog.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<AgentModelCredential | undefined>(() => {
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<string | null>(null)
const [visibility, setVisibility] = useState<AgentVisibility>(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 <Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent ref={scopeRef} className="max-h-[90dvh] overflow-y-auto">
<form onSubmit={(event) => {
if (event.target !== event.currentTarget) return
event.preventDefault()
if (!valid || props.pending) return
let extra: Record<string, unknown>
Expand All @@ -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 } : {}) },
})
}}>
<DialogHeader>
Expand All @@ -74,8 +93,14 @@ export function CreateAgentDialog(props: CreateAgentDialogProps) {
<div><Label htmlFor={`${id}-name`}>{t("agents.core.name")}</Label><Input id={`${id}-name`} value={name} onChange={(event) => setName(event.target.value)} disabled={props.pending} required /></div>
<div><Label htmlFor={`${id}-description`}>{t("agents.core.summary")}</Label><Input id={`${id}-description`} value={description} onChange={(event) => setDescription(event.target.value)} disabled={props.pending} /></div>
<AgentModelField workspaceID={props.workspaceID} value={modelID} legacyModel={String(props.agent?.config?.model ?? "")} harness={execution.harness} onChange={setModelID} disabled={props.pending} />
<AgentModelCredentialField workspaceID={props.workspaceID} value={modelCredential} onChange={setModelCredential} publicAgent={visibility === "public" || isClone} />
<AgentExecutionFields workspaceID={props.workspaceID} harness={execution.harness} environment={execution.environment} onHarnessChange={harness => setExecution(value => ({ ...value, harness }))} onEnvironmentChange={environment => setExecution(value => ({ ...value, environment }))} disabled={props.pending} />
{model && (!supportsCatalogModel(model, execution.harness) || execution.environment.type !== "openai_hosted") && <p role="alert" className="text-sm text-danger">{t("catalog.incompatible")}</p>}
{resources.selection?.some(item => item.capability.type === "skill") && execution.environment.type === "openai_hosted" && execution.environment.environment_template_id && <p role="status" className="text-sm text-fg-muted">{t("agentResources.templateSkillsPending")}</p>}
{isClone && <p className="text-sm text-fg-muted">{t("agents.form.clone.credentials")}</p>}
<AgentResourceFields hideCredentials={isClone} workspaceID={props.workspaceID} draft={resources} defaults={props.agent?.config} publicAgent={visibility === "public"} disabled={props.pending} />
{isClone && <AgentCloneCredentials credentials={cloneCredentials} workspaceID={props.workspaceID} failed={!!cloneSecrets.error || cloneCredentials.failed} fetching={cloneSecrets.isFetching || !cloneCredentials.ready} onRefresh={() => { void cloneSecrets.refetch(); cloneCredentials.retry() }} />}
<AgentConnectionsField workspaceID={props.workspaceID} agent={props.mode === "edit" ? props.agent : undefined} />
<AgentInstructionsField value={instructions} onChange={setInstructions} disabled={props.pending} />
{props.mode === "create" && <AgentVisibilityField value={visibility} onChange={setVisibility} disabled={props.pending} />}
<details><summary className="cursor-pointer text-sm font-medium">{t("core.advanced")}</summary><p className="my-2 text-xs text-fg-muted">{t("core.agentAdvancedHint")}</p><Textarea aria-label={t("core.advanced")} value={advanced} onChange={event => setAdvanced(event.target.value)} disabled={props.pending} spellCheck={false} /></details>
Expand Down
Loading
Loading