mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Gateway/UI: data-driven agents tools catalog with provenance (openclaw#24199) thanks @Takhoffman
Verified: - pnpm install --frozen-lockfile - pnpm build - gh pr checks 24199 --watch --fail-fast Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
- Control UI/Agents: make the Tools panel data-driven from runtime `tools.catalog`, add per-tool provenance labels (`core` / `plugin:<id>` + optional marker), and keep a static fallback list when the runtime catalog is unavailable.
|
||||||
- Control UI/Cron: add full web cron edit parity (including clone and richer validation/help text), plus all-jobs run history with pagination/search/sort/multi-filter controls and improved cron page layout for cleaner scheduling and failure triage workflows.
|
- Control UI/Cron: add full web cron edit parity (including clone and richer validation/help text), plus all-jobs run history with pagination/search/sort/multi-filter controls and improved cron page layout for cleaner scheduling and failure triage workflows.
|
||||||
- Provider/Mistral: add support for the Mistral provider, including memory embeddings and voice support. (#23845) Thanks @vincentkoc.
|
- Provider/Mistral: add support for the Mistral provider, including memory embeddings and voice support. (#23845) Thanks @vincentkoc.
|
||||||
- Update/Core: add an optional built-in auto-updater for package installs (`update.auto.*`), default-off, with stable rollout delay+jitter and beta hourly cadence.
|
- Update/Core: add an optional built-in auto-updater for package installs (`update.auto.*`), default-off, with stable rollout delay+jitter and beta hourly cadence.
|
||||||
|
|||||||
@@ -2170,6 +2170,132 @@ public struct SkillsStatusParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ToolsCatalogParams: Codable, Sendable {
|
||||||
|
public let agentid: String?
|
||||||
|
public let includeplugins: Bool?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String?,
|
||||||
|
includeplugins: Bool?)
|
||||||
|
{
|
||||||
|
self.agentid = agentid
|
||||||
|
self.includeplugins = includeplugins
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case includeplugins = "includePlugins"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogProfile: Codable, Sendable {
|
||||||
|
public let id: AnyCodable
|
||||||
|
public let label: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: AnyCodable,
|
||||||
|
label: String)
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogEntry: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let label: String
|
||||||
|
public let description: String
|
||||||
|
public let source: AnyCodable
|
||||||
|
public let pluginid: String?
|
||||||
|
public let optional: Bool?
|
||||||
|
public let defaultprofiles: [AnyCodable]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
description: String,
|
||||||
|
source: AnyCodable,
|
||||||
|
pluginid: String?,
|
||||||
|
optional: Bool?,
|
||||||
|
defaultprofiles: [AnyCodable])
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
self.description = description
|
||||||
|
self.source = source
|
||||||
|
self.pluginid = pluginid
|
||||||
|
self.optional = optional
|
||||||
|
self.defaultprofiles = defaultprofiles
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
case description
|
||||||
|
case source
|
||||||
|
case pluginid = "pluginId"
|
||||||
|
case optional
|
||||||
|
case defaultprofiles = "defaultProfiles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogGroup: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let label: String
|
||||||
|
public let source: AnyCodable
|
||||||
|
public let pluginid: String?
|
||||||
|
public let tools: [ToolCatalogEntry]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
source: AnyCodable,
|
||||||
|
pluginid: String?,
|
||||||
|
tools: [ToolCatalogEntry])
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
self.source = source
|
||||||
|
self.pluginid = pluginid
|
||||||
|
self.tools = tools
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
case source
|
||||||
|
case pluginid = "pluginId"
|
||||||
|
case tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolsCatalogResult: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let profiles: [ToolCatalogProfile]
|
||||||
|
public let groups: [ToolCatalogGroup]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
profiles: [ToolCatalogProfile],
|
||||||
|
groups: [ToolCatalogGroup])
|
||||||
|
{
|
||||||
|
self.agentid = agentid
|
||||||
|
self.profiles = profiles
|
||||||
|
self.groups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case profiles
|
||||||
|
case groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct SkillsBinsParams: Codable, Sendable {}
|
public struct SkillsBinsParams: Codable, Sendable {}
|
||||||
|
|
||||||
public struct SkillsBinsResult: Codable, Sendable {
|
public struct SkillsBinsResult: Codable, Sendable {
|
||||||
@@ -2306,15 +2432,39 @@ public struct CronJob: Codable, Sendable {
|
|||||||
|
|
||||||
public struct CronListParams: Codable, Sendable {
|
public struct CronListParams: Codable, Sendable {
|
||||||
public let includedisabled: Bool?
|
public let includedisabled: Bool?
|
||||||
|
public let limit: Int?
|
||||||
|
public let offset: Int?
|
||||||
|
public let query: String?
|
||||||
|
public let enabled: AnyCodable?
|
||||||
|
public let sortby: AnyCodable?
|
||||||
|
public let sortdir: AnyCodable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
includedisabled: Bool?)
|
includedisabled: Bool?,
|
||||||
|
limit: Int?,
|
||||||
|
offset: Int?,
|
||||||
|
query: String?,
|
||||||
|
enabled: AnyCodable?,
|
||||||
|
sortby: AnyCodable?,
|
||||||
|
sortdir: AnyCodable?)
|
||||||
{
|
{
|
||||||
self.includedisabled = includedisabled
|
self.includedisabled = includedisabled
|
||||||
|
self.limit = limit
|
||||||
|
self.offset = offset
|
||||||
|
self.query = query
|
||||||
|
self.enabled = enabled
|
||||||
|
self.sortby = sortby
|
||||||
|
self.sortdir = sortdir
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case includedisabled = "includeDisabled"
|
case includedisabled = "includeDisabled"
|
||||||
|
case limit
|
||||||
|
case offset
|
||||||
|
case query
|
||||||
|
case enabled
|
||||||
|
case sortby = "sortBy"
|
||||||
|
case sortdir = "sortDir"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2374,6 +2524,60 @@ public struct CronAddParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct CronRunsParams: Codable, Sendable {
|
||||||
|
public let scope: AnyCodable?
|
||||||
|
public let id: String?
|
||||||
|
public let jobid: String?
|
||||||
|
public let limit: Int?
|
||||||
|
public let offset: Int?
|
||||||
|
public let statuses: [AnyCodable]?
|
||||||
|
public let status: AnyCodable?
|
||||||
|
public let deliverystatuses: [AnyCodable]?
|
||||||
|
public let deliverystatus: AnyCodable?
|
||||||
|
public let query: String?
|
||||||
|
public let sortdir: AnyCodable?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
scope: AnyCodable?,
|
||||||
|
id: String?,
|
||||||
|
jobid: String?,
|
||||||
|
limit: Int?,
|
||||||
|
offset: Int?,
|
||||||
|
statuses: [AnyCodable]?,
|
||||||
|
status: AnyCodable?,
|
||||||
|
deliverystatuses: [AnyCodable]?,
|
||||||
|
deliverystatus: AnyCodable?,
|
||||||
|
query: String?,
|
||||||
|
sortdir: AnyCodable?)
|
||||||
|
{
|
||||||
|
self.scope = scope
|
||||||
|
self.id = id
|
||||||
|
self.jobid = jobid
|
||||||
|
self.limit = limit
|
||||||
|
self.offset = offset
|
||||||
|
self.statuses = statuses
|
||||||
|
self.status = status
|
||||||
|
self.deliverystatuses = deliverystatuses
|
||||||
|
self.deliverystatus = deliverystatus
|
||||||
|
self.query = query
|
||||||
|
self.sortdir = sortdir
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case scope
|
||||||
|
case id
|
||||||
|
case jobid = "jobId"
|
||||||
|
case limit
|
||||||
|
case offset
|
||||||
|
case statuses
|
||||||
|
case status
|
||||||
|
case deliverystatuses = "deliveryStatuses"
|
||||||
|
case deliverystatus = "deliveryStatus"
|
||||||
|
case query
|
||||||
|
case sortdir = "sortDir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct CronRunLogEntry: Codable, Sendable {
|
public struct CronRunLogEntry: Codable, Sendable {
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
public let jobid: String
|
public let jobid: String
|
||||||
@@ -2389,6 +2593,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
public let runatms: Int?
|
public let runatms: Int?
|
||||||
public let durationms: Int?
|
public let durationms: Int?
|
||||||
public let nextrunatms: Int?
|
public let nextrunatms: Int?
|
||||||
|
public let model: String?
|
||||||
|
public let provider: String?
|
||||||
|
public let usage: [String: AnyCodable]?
|
||||||
|
public let jobname: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
ts: Int,
|
ts: Int,
|
||||||
@@ -2404,7 +2612,11 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
sessionkey: String?,
|
sessionkey: String?,
|
||||||
runatms: Int?,
|
runatms: Int?,
|
||||||
durationms: Int?,
|
durationms: Int?,
|
||||||
nextrunatms: Int?)
|
nextrunatms: Int?,
|
||||||
|
model: String?,
|
||||||
|
provider: String?,
|
||||||
|
usage: [String: AnyCodable]?,
|
||||||
|
jobname: String?)
|
||||||
{
|
{
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
self.jobid = jobid
|
self.jobid = jobid
|
||||||
@@ -2420,6 +2632,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
self.runatms = runatms
|
self.runatms = runatms
|
||||||
self.durationms = durationms
|
self.durationms = durationms
|
||||||
self.nextrunatms = nextrunatms
|
self.nextrunatms = nextrunatms
|
||||||
|
self.model = model
|
||||||
|
self.provider = provider
|
||||||
|
self.usage = usage
|
||||||
|
self.jobname = jobname
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
@@ -2437,6 +2653,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
case runatms = "runAtMs"
|
case runatms = "runAtMs"
|
||||||
case durationms = "durationMs"
|
case durationms = "durationMs"
|
||||||
case nextrunatms = "nextRunAtMs"
|
case nextrunatms = "nextRunAtMs"
|
||||||
|
case model
|
||||||
|
case provider
|
||||||
|
case usage
|
||||||
|
case jobname = "jobName"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2170,6 +2170,132 @@ public struct SkillsStatusParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ToolsCatalogParams: Codable, Sendable {
|
||||||
|
public let agentid: String?
|
||||||
|
public let includeplugins: Bool?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String?,
|
||||||
|
includeplugins: Bool?)
|
||||||
|
{
|
||||||
|
self.agentid = agentid
|
||||||
|
self.includeplugins = includeplugins
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case includeplugins = "includePlugins"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogProfile: Codable, Sendable {
|
||||||
|
public let id: AnyCodable
|
||||||
|
public let label: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: AnyCodable,
|
||||||
|
label: String)
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogEntry: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let label: String
|
||||||
|
public let description: String
|
||||||
|
public let source: AnyCodable
|
||||||
|
public let pluginid: String?
|
||||||
|
public let optional: Bool?
|
||||||
|
public let defaultprofiles: [AnyCodable]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
description: String,
|
||||||
|
source: AnyCodable,
|
||||||
|
pluginid: String?,
|
||||||
|
optional: Bool?,
|
||||||
|
defaultprofiles: [AnyCodable])
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
self.description = description
|
||||||
|
self.source = source
|
||||||
|
self.pluginid = pluginid
|
||||||
|
self.optional = optional
|
||||||
|
self.defaultprofiles = defaultprofiles
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
case description
|
||||||
|
case source
|
||||||
|
case pluginid = "pluginId"
|
||||||
|
case optional
|
||||||
|
case defaultprofiles = "defaultProfiles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolCatalogGroup: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let label: String
|
||||||
|
public let source: AnyCodable
|
||||||
|
public let pluginid: String?
|
||||||
|
public let tools: [ToolCatalogEntry]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
source: AnyCodable,
|
||||||
|
pluginid: String?,
|
||||||
|
tools: [ToolCatalogEntry])
|
||||||
|
{
|
||||||
|
self.id = id
|
||||||
|
self.label = label
|
||||||
|
self.source = source
|
||||||
|
self.pluginid = pluginid
|
||||||
|
self.tools = tools
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case label
|
||||||
|
case source
|
||||||
|
case pluginid = "pluginId"
|
||||||
|
case tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ToolsCatalogResult: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let profiles: [ToolCatalogProfile]
|
||||||
|
public let groups: [ToolCatalogGroup]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
profiles: [ToolCatalogProfile],
|
||||||
|
groups: [ToolCatalogGroup])
|
||||||
|
{
|
||||||
|
self.agentid = agentid
|
||||||
|
self.profiles = profiles
|
||||||
|
self.groups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case profiles
|
||||||
|
case groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct SkillsBinsParams: Codable, Sendable {}
|
public struct SkillsBinsParams: Codable, Sendable {}
|
||||||
|
|
||||||
public struct SkillsBinsResult: Codable, Sendable {
|
public struct SkillsBinsResult: Codable, Sendable {
|
||||||
@@ -2306,15 +2432,39 @@ public struct CronJob: Codable, Sendable {
|
|||||||
|
|
||||||
public struct CronListParams: Codable, Sendable {
|
public struct CronListParams: Codable, Sendable {
|
||||||
public let includedisabled: Bool?
|
public let includedisabled: Bool?
|
||||||
|
public let limit: Int?
|
||||||
|
public let offset: Int?
|
||||||
|
public let query: String?
|
||||||
|
public let enabled: AnyCodable?
|
||||||
|
public let sortby: AnyCodable?
|
||||||
|
public let sortdir: AnyCodable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
includedisabled: Bool?)
|
includedisabled: Bool?,
|
||||||
|
limit: Int?,
|
||||||
|
offset: Int?,
|
||||||
|
query: String?,
|
||||||
|
enabled: AnyCodable?,
|
||||||
|
sortby: AnyCodable?,
|
||||||
|
sortdir: AnyCodable?)
|
||||||
{
|
{
|
||||||
self.includedisabled = includedisabled
|
self.includedisabled = includedisabled
|
||||||
|
self.limit = limit
|
||||||
|
self.offset = offset
|
||||||
|
self.query = query
|
||||||
|
self.enabled = enabled
|
||||||
|
self.sortby = sortby
|
||||||
|
self.sortdir = sortdir
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case includedisabled = "includeDisabled"
|
case includedisabled = "includeDisabled"
|
||||||
|
case limit
|
||||||
|
case offset
|
||||||
|
case query
|
||||||
|
case enabled
|
||||||
|
case sortby = "sortBy"
|
||||||
|
case sortdir = "sortDir"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2374,6 +2524,60 @@ public struct CronAddParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct CronRunsParams: Codable, Sendable {
|
||||||
|
public let scope: AnyCodable?
|
||||||
|
public let id: String?
|
||||||
|
public let jobid: String?
|
||||||
|
public let limit: Int?
|
||||||
|
public let offset: Int?
|
||||||
|
public let statuses: [AnyCodable]?
|
||||||
|
public let status: AnyCodable?
|
||||||
|
public let deliverystatuses: [AnyCodable]?
|
||||||
|
public let deliverystatus: AnyCodable?
|
||||||
|
public let query: String?
|
||||||
|
public let sortdir: AnyCodable?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
scope: AnyCodable?,
|
||||||
|
id: String?,
|
||||||
|
jobid: String?,
|
||||||
|
limit: Int?,
|
||||||
|
offset: Int?,
|
||||||
|
statuses: [AnyCodable]?,
|
||||||
|
status: AnyCodable?,
|
||||||
|
deliverystatuses: [AnyCodable]?,
|
||||||
|
deliverystatus: AnyCodable?,
|
||||||
|
query: String?,
|
||||||
|
sortdir: AnyCodable?)
|
||||||
|
{
|
||||||
|
self.scope = scope
|
||||||
|
self.id = id
|
||||||
|
self.jobid = jobid
|
||||||
|
self.limit = limit
|
||||||
|
self.offset = offset
|
||||||
|
self.statuses = statuses
|
||||||
|
self.status = status
|
||||||
|
self.deliverystatuses = deliverystatuses
|
||||||
|
self.deliverystatus = deliverystatus
|
||||||
|
self.query = query
|
||||||
|
self.sortdir = sortdir
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case scope
|
||||||
|
case id
|
||||||
|
case jobid = "jobId"
|
||||||
|
case limit
|
||||||
|
case offset
|
||||||
|
case statuses
|
||||||
|
case status
|
||||||
|
case deliverystatuses = "deliveryStatuses"
|
||||||
|
case deliverystatus = "deliveryStatus"
|
||||||
|
case query
|
||||||
|
case sortdir = "sortDir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct CronRunLogEntry: Codable, Sendable {
|
public struct CronRunLogEntry: Codable, Sendable {
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
public let jobid: String
|
public let jobid: String
|
||||||
@@ -2389,6 +2593,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
public let runatms: Int?
|
public let runatms: Int?
|
||||||
public let durationms: Int?
|
public let durationms: Int?
|
||||||
public let nextrunatms: Int?
|
public let nextrunatms: Int?
|
||||||
|
public let model: String?
|
||||||
|
public let provider: String?
|
||||||
|
public let usage: [String: AnyCodable]?
|
||||||
|
public let jobname: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
ts: Int,
|
ts: Int,
|
||||||
@@ -2404,7 +2612,11 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
sessionkey: String?,
|
sessionkey: String?,
|
||||||
runatms: Int?,
|
runatms: Int?,
|
||||||
durationms: Int?,
|
durationms: Int?,
|
||||||
nextrunatms: Int?)
|
nextrunatms: Int?,
|
||||||
|
model: String?,
|
||||||
|
provider: String?,
|
||||||
|
usage: [String: AnyCodable]?,
|
||||||
|
jobname: String?)
|
||||||
{
|
{
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
self.jobid = jobid
|
self.jobid = jobid
|
||||||
@@ -2420,6 +2632,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
self.runatms = runatms
|
self.runatms = runatms
|
||||||
self.durationms = durationms
|
self.durationms = durationms
|
||||||
self.nextrunatms = nextrunatms
|
self.nextrunatms = nextrunatms
|
||||||
|
self.model = model
|
||||||
|
self.provider = provider
|
||||||
|
self.usage = usage
|
||||||
|
self.jobname = jobname
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
@@ -2437,6 +2653,10 @@ public struct CronRunLogEntry: Codable, Sendable {
|
|||||||
case runatms = "runAtMs"
|
case runatms = "runAtMs"
|
||||||
case durationms = "durationMs"
|
case durationms = "durationMs"
|
||||||
case nextrunatms = "nextRunAtMs"
|
case nextrunatms = "nextRunAtMs"
|
||||||
|
case model
|
||||||
|
case provider
|
||||||
|
case usage
|
||||||
|
case jobname = "jobName"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,14 @@ The Gateway treats these as **claims** and enforces server-side allowlists.
|
|||||||
- Nodes may call `skills.bins` to fetch the current list of skill executables
|
- Nodes may call `skills.bins` to fetch the current list of skill executables
|
||||||
for auto-allow checks.
|
for auto-allow checks.
|
||||||
|
|
||||||
|
### Operator helper methods
|
||||||
|
|
||||||
|
- Operators may call `tools.catalog` (`operator.read`) to fetch the runtime tool catalog for an
|
||||||
|
agent. The response includes grouped tools and provenance metadata:
|
||||||
|
- `source`: `core` or `plugin`
|
||||||
|
- `pluginId`: plugin owner when `source="plugin"`
|
||||||
|
- `optional`: whether a plugin tool is optional
|
||||||
|
|
||||||
## Exec approvals
|
## Exec approvals
|
||||||
|
|
||||||
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.
|
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
|
|||||||
- History is always fetched from the gateway (no local file watching).
|
- History is always fetched from the gateway (no local file watching).
|
||||||
- If the gateway is unreachable, WebChat is read-only.
|
- If the gateway is unreachable, WebChat is read-only.
|
||||||
|
|
||||||
|
## Control UI agents tools panel
|
||||||
|
|
||||||
|
- The Control UI `/agents` Tools panel fetches a runtime catalog via `tools.catalog` and labels each
|
||||||
|
tool as `core` or `plugin:<id>` (plus `optional` for optional plugin tools).
|
||||||
|
- If `tools.catalog` is unavailable, the panel falls back to a built-in static list.
|
||||||
|
- The panel edits profile and override config, but effective runtime access still follows policy
|
||||||
|
precedence (`allow`/`deny`, per-agent and provider/channel overrides).
|
||||||
|
|
||||||
## Remote use
|
## Remote use
|
||||||
|
|
||||||
- Remote mode tunnels the gateway WebSocket over SSH/Tailscale.
|
- Remote mode tunnels the gateway WebSocket over SSH/Tailscale.
|
||||||
|
|||||||
322
src/agents/tool-catalog.ts
Normal file
322
src/agents/tool-catalog.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
||||||
|
|
||||||
|
type ToolProfilePolicy = {
|
||||||
|
allow?: string[];
|
||||||
|
deny?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreToolSection = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
tools: Array<{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CoreToolDefinition = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
sectionId: string;
|
||||||
|
profiles: ToolProfileId[];
|
||||||
|
includeInOpenClawGroup?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CORE_TOOL_SECTION_ORDER: Array<{ id: string; label: string }> = [
|
||||||
|
{ id: "fs", label: "Files" },
|
||||||
|
{ id: "runtime", label: "Runtime" },
|
||||||
|
{ id: "web", label: "Web" },
|
||||||
|
{ id: "memory", label: "Memory" },
|
||||||
|
{ id: "sessions", label: "Sessions" },
|
||||||
|
{ id: "ui", label: "UI" },
|
||||||
|
{ id: "messaging", label: "Messaging" },
|
||||||
|
{ id: "automation", label: "Automation" },
|
||||||
|
{ id: "nodes", label: "Nodes" },
|
||||||
|
{ id: "agents", label: "Agents" },
|
||||||
|
{ id: "media", label: "Media" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "read",
|
||||||
|
label: "read",
|
||||||
|
description: "Read file contents",
|
||||||
|
sectionId: "fs",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "write",
|
||||||
|
label: "write",
|
||||||
|
description: "Create or overwrite files",
|
||||||
|
sectionId: "fs",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edit",
|
||||||
|
label: "edit",
|
||||||
|
description: "Make precise edits",
|
||||||
|
sectionId: "fs",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "apply_patch",
|
||||||
|
label: "apply_patch",
|
||||||
|
description: "Patch files (OpenAI)",
|
||||||
|
sectionId: "fs",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exec",
|
||||||
|
label: "exec",
|
||||||
|
description: "Run shell commands",
|
||||||
|
sectionId: "runtime",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process",
|
||||||
|
label: "process",
|
||||||
|
description: "Manage background processes",
|
||||||
|
sectionId: "runtime",
|
||||||
|
profiles: ["coding"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "web_search",
|
||||||
|
label: "web_search",
|
||||||
|
description: "Search the web",
|
||||||
|
sectionId: "web",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "web_fetch",
|
||||||
|
label: "web_fetch",
|
||||||
|
description: "Fetch web content",
|
||||||
|
sectionId: "web",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "memory_search",
|
||||||
|
label: "memory_search",
|
||||||
|
description: "Semantic search",
|
||||||
|
sectionId: "memory",
|
||||||
|
profiles: ["coding"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "memory_get",
|
||||||
|
label: "memory_get",
|
||||||
|
description: "Read memory files",
|
||||||
|
sectionId: "memory",
|
||||||
|
profiles: ["coding"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sessions_list",
|
||||||
|
label: "sessions_list",
|
||||||
|
description: "List sessions",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["coding", "messaging"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sessions_history",
|
||||||
|
label: "sessions_history",
|
||||||
|
description: "Session history",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["coding", "messaging"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sessions_send",
|
||||||
|
label: "sessions_send",
|
||||||
|
description: "Send to session",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["coding", "messaging"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sessions_spawn",
|
||||||
|
label: "sessions_spawn",
|
||||||
|
description: "Spawn sub-agent",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["coding"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "subagents",
|
||||||
|
label: "subagents",
|
||||||
|
description: "Manage sub-agents",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["coding"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session_status",
|
||||||
|
label: "session_status",
|
||||||
|
description: "Session status",
|
||||||
|
sectionId: "sessions",
|
||||||
|
profiles: ["minimal", "coding", "messaging"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browser",
|
||||||
|
label: "browser",
|
||||||
|
description: "Control web browser",
|
||||||
|
sectionId: "ui",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "canvas",
|
||||||
|
label: "canvas",
|
||||||
|
description: "Control canvases",
|
||||||
|
sectionId: "ui",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "message",
|
||||||
|
label: "message",
|
||||||
|
description: "Send messages",
|
||||||
|
sectionId: "messaging",
|
||||||
|
profiles: ["messaging"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cron",
|
||||||
|
label: "cron",
|
||||||
|
description: "Schedule tasks",
|
||||||
|
sectionId: "automation",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gateway",
|
||||||
|
label: "gateway",
|
||||||
|
description: "Gateway control",
|
||||||
|
sectionId: "automation",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nodes",
|
||||||
|
label: "nodes",
|
||||||
|
description: "Nodes + devices",
|
||||||
|
sectionId: "nodes",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agents_list",
|
||||||
|
label: "agents_list",
|
||||||
|
description: "List agents",
|
||||||
|
sectionId: "agents",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "image",
|
||||||
|
label: "image",
|
||||||
|
description: "Image understanding",
|
||||||
|
sectionId: "media",
|
||||||
|
profiles: ["coding"],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tts",
|
||||||
|
label: "tts",
|
||||||
|
description: "Text-to-speech conversion",
|
||||||
|
sectionId: "media",
|
||||||
|
profiles: [],
|
||||||
|
includeInOpenClawGroup: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CORE_TOOL_BY_ID = new Map<string, CoreToolDefinition>(
|
||||||
|
CORE_TOOL_DEFINITIONS.map((tool) => [tool.id, tool]),
|
||||||
|
);
|
||||||
|
|
||||||
|
function listCoreToolIdsForProfile(profile: ToolProfileId): string[] {
|
||||||
|
return CORE_TOOL_DEFINITIONS.filter((tool) => tool.profiles.includes(profile)).map(
|
||||||
|
(tool) => tool.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORE_TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
|
||||||
|
minimal: {
|
||||||
|
allow: listCoreToolIdsForProfile("minimal"),
|
||||||
|
},
|
||||||
|
coding: {
|
||||||
|
allow: listCoreToolIdsForProfile("coding"),
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
allow: listCoreToolIdsForProfile("messaging"),
|
||||||
|
},
|
||||||
|
full: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildCoreToolGroupMap() {
|
||||||
|
const sectionToolMap = new Map<string, string[]>();
|
||||||
|
for (const tool of CORE_TOOL_DEFINITIONS) {
|
||||||
|
const groupId = `group:${tool.sectionId}`;
|
||||||
|
const list = sectionToolMap.get(groupId) ?? [];
|
||||||
|
list.push(tool.id);
|
||||||
|
sectionToolMap.set(groupId, list);
|
||||||
|
}
|
||||||
|
const openclawTools = CORE_TOOL_DEFINITIONS.filter((tool) => tool.includeInOpenClawGroup).map(
|
||||||
|
(tool) => tool.id,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
"group:openclaw": openclawTools,
|
||||||
|
...Object.fromEntries(sectionToolMap.entries()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CORE_TOOL_GROUPS = buildCoreToolGroupMap();
|
||||||
|
|
||||||
|
export const PROFILE_OPTIONS = [
|
||||||
|
{ id: "minimal", label: "Minimal" },
|
||||||
|
{ id: "coding", label: "Coding" },
|
||||||
|
{ id: "messaging", label: "Messaging" },
|
||||||
|
{ id: "full", label: "Full" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function resolveCoreToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
|
||||||
|
if (!profile) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const resolved = CORE_TOOL_PROFILES[profile as ToolProfileId];
|
||||||
|
if (!resolved) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!resolved.allow && !resolved.deny) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
allow: resolved.allow ? [...resolved.allow] : undefined,
|
||||||
|
deny: resolved.deny ? [...resolved.deny] : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listCoreToolSections(): CoreToolSection[] {
|
||||||
|
return CORE_TOOL_SECTION_ORDER.map((section) => ({
|
||||||
|
id: section.id,
|
||||||
|
label: section.label,
|
||||||
|
tools: CORE_TOOL_DEFINITIONS.filter((tool) => tool.sectionId === section.id).map((tool) => ({
|
||||||
|
id: tool.id,
|
||||||
|
label: tool.label,
|
||||||
|
description: tool.description,
|
||||||
|
})),
|
||||||
|
})).filter((section) => section.tools.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCoreToolProfiles(toolId: string): ToolProfileId[] {
|
||||||
|
const tool = CORE_TOOL_BY_ID.get(toolId);
|
||||||
|
if (!tool) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...tool.profiles];
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
import {
|
||||||
|
CORE_TOOL_GROUPS,
|
||||||
|
resolveCoreToolProfilePolicy,
|
||||||
|
type ToolProfileId,
|
||||||
|
} from "./tool-catalog.js";
|
||||||
|
|
||||||
type ToolProfilePolicy = {
|
type ToolProfilePolicy = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
@@ -10,72 +14,7 @@ const TOOL_NAME_ALIASES: Record<string, string> = {
|
|||||||
"apply-patch": "apply_patch",
|
"apply-patch": "apply_patch",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
export const TOOL_GROUPS: Record<string, string[]> = { ...CORE_TOOL_GROUPS };
|
||||||
// NOTE: Keep canonical (lowercase) tool names here.
|
|
||||||
"group:memory": ["memory_search", "memory_get"],
|
|
||||||
"group:web": ["web_search", "web_fetch"],
|
|
||||||
// Basic workspace/file tools
|
|
||||||
"group:fs": ["read", "write", "edit", "apply_patch"],
|
|
||||||
// Host/runtime execution tools
|
|
||||||
"group:runtime": ["exec", "process"],
|
|
||||||
// Session management tools
|
|
||||||
"group:sessions": [
|
|
||||||
"sessions_list",
|
|
||||||
"sessions_history",
|
|
||||||
"sessions_send",
|
|
||||||
"sessions_spawn",
|
|
||||||
"subagents",
|
|
||||||
"session_status",
|
|
||||||
],
|
|
||||||
// UI helpers
|
|
||||||
"group:ui": ["browser", "canvas"],
|
|
||||||
// Automation + infra
|
|
||||||
"group:automation": ["cron", "gateway"],
|
|
||||||
// Messaging surface
|
|
||||||
"group:messaging": ["message"],
|
|
||||||
// Nodes + device tools
|
|
||||||
"group:nodes": ["nodes"],
|
|
||||||
// All OpenClaw native tools (excludes provider plugins).
|
|
||||||
"group:openclaw": [
|
|
||||||
"browser",
|
|
||||||
"canvas",
|
|
||||||
"nodes",
|
|
||||||
"cron",
|
|
||||||
"message",
|
|
||||||
"gateway",
|
|
||||||
"agents_list",
|
|
||||||
"sessions_list",
|
|
||||||
"sessions_history",
|
|
||||||
"sessions_send",
|
|
||||||
"sessions_spawn",
|
|
||||||
"subagents",
|
|
||||||
"session_status",
|
|
||||||
"memory_search",
|
|
||||||
"memory_get",
|
|
||||||
"web_search",
|
|
||||||
"web_fetch",
|
|
||||||
"image",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
|
|
||||||
minimal: {
|
|
||||||
allow: ["session_status"],
|
|
||||||
},
|
|
||||||
coding: {
|
|
||||||
allow: ["group:fs", "group:runtime", "group:sessions", "group:memory", "image"],
|
|
||||||
},
|
|
||||||
messaging: {
|
|
||||||
allow: [
|
|
||||||
"group:messaging",
|
|
||||||
"sessions_list",
|
|
||||||
"sessions_history",
|
|
||||||
"sessions_send",
|
|
||||||
"session_status",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
full: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function normalizeToolName(name: string) {
|
export function normalizeToolName(name: string) {
|
||||||
const normalized = name.trim().toLowerCase();
|
const normalized = name.trim().toLowerCase();
|
||||||
@@ -104,18 +43,7 @@ export function expandToolGroups(list?: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
|
export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
|
||||||
if (!profile) {
|
return resolveCoreToolProfilePolicy(profile);
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const resolved = TOOL_PROFILES[profile as ToolProfileId];
|
|
||||||
if (!resolved) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (!resolved.allow && !resolved.deny) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
allow: resolved.allow ? [...resolved.allow] : undefined,
|
|
||||||
deny: resolved.deny ? [...resolved.deny] : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { ToolProfileId };
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe("tool-policy", () => {
|
|||||||
|
|
||||||
it("resolves known profiles and ignores unknown ones", () => {
|
it("resolves known profiles and ignores unknown ones", () => {
|
||||||
const coding = resolveToolProfilePolicy("coding");
|
const coding = resolveToolProfilePolicy("coding");
|
||||||
expect(coding?.allow).toContain("group:fs");
|
expect(coding?.allow).toContain("read");
|
||||||
expect(resolveToolProfilePolicy("nope")).toBeUndefined();
|
expect(resolveToolProfilePolicy("nope")).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ describe("tool-policy", () => {
|
|||||||
expect(group).toContain("message");
|
expect(group).toContain("message");
|
||||||
expect(group).toContain("subagents");
|
expect(group).toContain("subagents");
|
||||||
expect(group).toContain("session_status");
|
expect(group).toContain("session_status");
|
||||||
|
expect(group).toContain("tts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes tool names and aliases", () => {
|
it("normalizes tool names and aliases", () => {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
|||||||
"tts.status",
|
"tts.status",
|
||||||
"tts.providers",
|
"tts.providers",
|
||||||
"models.list",
|
"models.list",
|
||||||
|
"tools.catalog",
|
||||||
"agents.list",
|
"agents.list",
|
||||||
"agent.identity.get",
|
"agent.identity.get",
|
||||||
"skills.status",
|
"skills.status",
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ import {
|
|||||||
SkillsStatusParamsSchema,
|
SkillsStatusParamsSchema,
|
||||||
type SkillsUpdateParams,
|
type SkillsUpdateParams,
|
||||||
SkillsUpdateParamsSchema,
|
SkillsUpdateParamsSchema,
|
||||||
|
type ToolsCatalogParams,
|
||||||
|
ToolsCatalogParamsSchema,
|
||||||
|
type ToolsCatalogResult,
|
||||||
type Snapshot,
|
type Snapshot,
|
||||||
SnapshotSchema,
|
SnapshotSchema,
|
||||||
type StateVersion,
|
type StateVersion,
|
||||||
@@ -319,6 +322,7 @@ export const validateChannelsLogoutParams = ajv.compile<ChannelsLogoutParams>(
|
|||||||
);
|
);
|
||||||
export const validateModelsListParams = ajv.compile<ModelsListParams>(ModelsListParamsSchema);
|
export const validateModelsListParams = ajv.compile<ModelsListParams>(ModelsListParamsSchema);
|
||||||
export const validateSkillsStatusParams = ajv.compile<SkillsStatusParams>(SkillsStatusParamsSchema);
|
export const validateSkillsStatusParams = ajv.compile<SkillsStatusParams>(SkillsStatusParamsSchema);
|
||||||
|
export const validateToolsCatalogParams = ajv.compile<ToolsCatalogParams>(ToolsCatalogParamsSchema);
|
||||||
export const validateSkillsBinsParams = ajv.compile<SkillsBinsParams>(SkillsBinsParamsSchema);
|
export const validateSkillsBinsParams = ajv.compile<SkillsBinsParams>(SkillsBinsParamsSchema);
|
||||||
export const validateSkillsInstallParams =
|
export const validateSkillsInstallParams =
|
||||||
ajv.compile<SkillsInstallParams>(SkillsInstallParamsSchema);
|
ajv.compile<SkillsInstallParams>(SkillsInstallParamsSchema);
|
||||||
@@ -487,6 +491,7 @@ export {
|
|||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
ModelsListParamsSchema,
|
ModelsListParamsSchema,
|
||||||
SkillsStatusParamsSchema,
|
SkillsStatusParamsSchema,
|
||||||
|
ToolsCatalogParamsSchema,
|
||||||
SkillsInstallParamsSchema,
|
SkillsInstallParamsSchema,
|
||||||
SkillsUpdateParamsSchema,
|
SkillsUpdateParamsSchema,
|
||||||
CronJobSchema,
|
CronJobSchema,
|
||||||
@@ -575,6 +580,8 @@ export type {
|
|||||||
AgentsListParams,
|
AgentsListParams,
|
||||||
AgentsListResult,
|
AgentsListResult,
|
||||||
SkillsStatusParams,
|
SkillsStatusParams,
|
||||||
|
ToolsCatalogParams,
|
||||||
|
ToolsCatalogResult,
|
||||||
SkillsBinsParams,
|
SkillsBinsParams,
|
||||||
SkillsBinsResult,
|
SkillsBinsResult,
|
||||||
SkillsInstallParams,
|
SkillsInstallParams,
|
||||||
|
|||||||
@@ -207,3 +207,64 @@ export const SkillsUpdateParamsSchema = Type.Object(
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ToolsCatalogParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: Type.Optional(NonEmptyString),
|
||||||
|
includePlugins: Type.Optional(Type.Boolean()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToolCatalogProfileSchema = Type.Object(
|
||||||
|
{
|
||||||
|
id: Type.Union([
|
||||||
|
Type.Literal("minimal"),
|
||||||
|
Type.Literal("coding"),
|
||||||
|
Type.Literal("messaging"),
|
||||||
|
Type.Literal("full"),
|
||||||
|
]),
|
||||||
|
label: NonEmptyString,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToolCatalogEntrySchema = Type.Object(
|
||||||
|
{
|
||||||
|
id: NonEmptyString,
|
||||||
|
label: NonEmptyString,
|
||||||
|
description: Type.String(),
|
||||||
|
source: Type.Union([Type.Literal("core"), Type.Literal("plugin")]),
|
||||||
|
pluginId: Type.Optional(NonEmptyString),
|
||||||
|
optional: Type.Optional(Type.Boolean()),
|
||||||
|
defaultProfiles: Type.Array(
|
||||||
|
Type.Union([
|
||||||
|
Type.Literal("minimal"),
|
||||||
|
Type.Literal("coding"),
|
||||||
|
Type.Literal("messaging"),
|
||||||
|
Type.Literal("full"),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToolCatalogGroupSchema = Type.Object(
|
||||||
|
{
|
||||||
|
id: NonEmptyString,
|
||||||
|
label: NonEmptyString,
|
||||||
|
source: Type.Union([Type.Literal("core"), Type.Literal("plugin")]),
|
||||||
|
pluginId: Type.Optional(NonEmptyString),
|
||||||
|
tools: Type.Array(ToolCatalogEntrySchema),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToolsCatalogResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
profiles: Type.Array(ToolCatalogProfileSchema),
|
||||||
|
groups: Type.Array(ToolCatalogGroupSchema),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ import {
|
|||||||
SkillsInstallParamsSchema,
|
SkillsInstallParamsSchema,
|
||||||
SkillsStatusParamsSchema,
|
SkillsStatusParamsSchema,
|
||||||
SkillsUpdateParamsSchema,
|
SkillsUpdateParamsSchema,
|
||||||
|
ToolCatalogEntrySchema,
|
||||||
|
ToolCatalogGroupSchema,
|
||||||
|
ToolCatalogProfileSchema,
|
||||||
|
ToolsCatalogParamsSchema,
|
||||||
|
ToolsCatalogResultSchema,
|
||||||
} from "./agents-models-skills.js";
|
} from "./agents-models-skills.js";
|
||||||
import {
|
import {
|
||||||
ChannelsLogoutParamsSchema,
|
ChannelsLogoutParamsSchema,
|
||||||
@@ -224,6 +229,11 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
|||||||
ModelsListParams: ModelsListParamsSchema,
|
ModelsListParams: ModelsListParamsSchema,
|
||||||
ModelsListResult: ModelsListResultSchema,
|
ModelsListResult: ModelsListResultSchema,
|
||||||
SkillsStatusParams: SkillsStatusParamsSchema,
|
SkillsStatusParams: SkillsStatusParamsSchema,
|
||||||
|
ToolsCatalogParams: ToolsCatalogParamsSchema,
|
||||||
|
ToolCatalogProfile: ToolCatalogProfileSchema,
|
||||||
|
ToolCatalogEntry: ToolCatalogEntrySchema,
|
||||||
|
ToolCatalogGroup: ToolCatalogGroupSchema,
|
||||||
|
ToolsCatalogResult: ToolsCatalogResultSchema,
|
||||||
SkillsBinsParams: SkillsBinsParamsSchema,
|
SkillsBinsParams: SkillsBinsParamsSchema,
|
||||||
SkillsBinsResult: SkillsBinsResultSchema,
|
SkillsBinsResult: SkillsBinsResultSchema,
|
||||||
SkillsInstallParams: SkillsInstallParamsSchema,
|
SkillsInstallParams: SkillsInstallParamsSchema,
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ import type {
|
|||||||
SkillsInstallParamsSchema,
|
SkillsInstallParamsSchema,
|
||||||
SkillsStatusParamsSchema,
|
SkillsStatusParamsSchema,
|
||||||
SkillsUpdateParamsSchema,
|
SkillsUpdateParamsSchema,
|
||||||
|
ToolCatalogEntrySchema,
|
||||||
|
ToolCatalogGroupSchema,
|
||||||
|
ToolCatalogProfileSchema,
|
||||||
|
ToolsCatalogParamsSchema,
|
||||||
|
ToolsCatalogResultSchema,
|
||||||
} from "./agents-models-skills.js";
|
} from "./agents-models-skills.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelsLogoutParamsSchema,
|
ChannelsLogoutParamsSchema,
|
||||||
@@ -213,6 +218,11 @@ export type ModelChoice = Static<typeof ModelChoiceSchema>;
|
|||||||
export type ModelsListParams = Static<typeof ModelsListParamsSchema>;
|
export type ModelsListParams = Static<typeof ModelsListParamsSchema>;
|
||||||
export type ModelsListResult = Static<typeof ModelsListResultSchema>;
|
export type ModelsListResult = Static<typeof ModelsListResultSchema>;
|
||||||
export type SkillsStatusParams = Static<typeof SkillsStatusParamsSchema>;
|
export type SkillsStatusParams = Static<typeof SkillsStatusParamsSchema>;
|
||||||
|
export type ToolsCatalogParams = Static<typeof ToolsCatalogParamsSchema>;
|
||||||
|
export type ToolCatalogProfile = Static<typeof ToolCatalogProfileSchema>;
|
||||||
|
export type ToolCatalogEntry = Static<typeof ToolCatalogEntrySchema>;
|
||||||
|
export type ToolCatalogGroup = Static<typeof ToolCatalogGroupSchema>;
|
||||||
|
export type ToolsCatalogResult = Static<typeof ToolsCatalogResultSchema>;
|
||||||
export type SkillsBinsParams = Static<typeof SkillsBinsParamsSchema>;
|
export type SkillsBinsParams = Static<typeof SkillsBinsParamsSchema>;
|
||||||
export type SkillsBinsResult = Static<typeof SkillsBinsResultSchema>;
|
export type SkillsBinsResult = Static<typeof SkillsBinsResultSchema>;
|
||||||
export type SkillsInstallParams = Static<typeof SkillsInstallParamsSchema>;
|
export type SkillsInstallParams = Static<typeof SkillsInstallParamsSchema>;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const BASE_METHODS = [
|
|||||||
"talk.config",
|
"talk.config",
|
||||||
"talk.mode",
|
"talk.mode",
|
||||||
"models.list",
|
"models.list",
|
||||||
|
"tools.catalog",
|
||||||
"agents.list",
|
"agents.list",
|
||||||
"agents.create",
|
"agents.create",
|
||||||
"agents.update",
|
"agents.update",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { sessionsHandlers } from "./server-methods/sessions.js";
|
|||||||
import { skillsHandlers } from "./server-methods/skills.js";
|
import { skillsHandlers } from "./server-methods/skills.js";
|
||||||
import { systemHandlers } from "./server-methods/system.js";
|
import { systemHandlers } from "./server-methods/system.js";
|
||||||
import { talkHandlers } from "./server-methods/talk.js";
|
import { talkHandlers } from "./server-methods/talk.js";
|
||||||
|
import { toolsCatalogHandlers } from "./server-methods/tools-catalog.js";
|
||||||
import { ttsHandlers } from "./server-methods/tts.js";
|
import { ttsHandlers } from "./server-methods/tts.js";
|
||||||
import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js";
|
import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js";
|
||||||
import { updateHandlers } from "./server-methods/update.js";
|
import { updateHandlers } from "./server-methods/update.js";
|
||||||
@@ -76,6 +77,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
|
|||||||
...configHandlers,
|
...configHandlers,
|
||||||
...wizardHandlers,
|
...wizardHandlers,
|
||||||
...talkHandlers,
|
...talkHandlers,
|
||||||
|
...toolsCatalogHandlers,
|
||||||
...ttsHandlers,
|
...ttsHandlers,
|
||||||
...skillsHandlers,
|
...skillsHandlers,
|
||||||
...sessionsHandlers,
|
...sessionsHandlers,
|
||||||
|
|||||||
120
src/gateway/server-methods/tools-catalog.test.ts
Normal file
120
src/gateway/server-methods/tools-catalog.test.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { ErrorCodes } from "../protocol/index.js";
|
||||||
|
import { toolsCatalogHandlers } from "./tools-catalog.js";
|
||||||
|
|
||||||
|
vi.mock("../../config/config.js", () => ({
|
||||||
|
loadConfig: vi.fn(() => ({})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/agent-scope.js", () => ({
|
||||||
|
listAgentIds: vi.fn(() => ["main"]),
|
||||||
|
resolveDefaultAgentId: vi.fn(() => "main"),
|
||||||
|
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace-main"),
|
||||||
|
resolveAgentDir: vi.fn(() => "/tmp/agents/main/agent"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pluginToolMetaState = new Map<string, { pluginId: string; optional: boolean }>();
|
||||||
|
|
||||||
|
vi.mock("../../plugins/tools.js", () => ({
|
||||||
|
resolvePluginTools: vi.fn(() => [
|
||||||
|
{ name: "voice_call", label: "voice_call", description: "Plugin calling tool" },
|
||||||
|
{ name: "matrix_room", label: "matrix_room", description: "Matrix room helper" },
|
||||||
|
]),
|
||||||
|
getPluginToolMeta: vi.fn((tool: { name: string }) => pluginToolMetaState.get(tool.name)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type RespondCall = [boolean, unknown?, { code: number; message: string }?];
|
||||||
|
|
||||||
|
function createInvokeParams(params: Record<string, unknown>) {
|
||||||
|
const respond = vi.fn();
|
||||||
|
return {
|
||||||
|
respond,
|
||||||
|
invoke: async () =>
|
||||||
|
await toolsCatalogHandlers["tools.catalog"]({
|
||||||
|
params,
|
||||||
|
respond: respond as never,
|
||||||
|
context: {} as never,
|
||||||
|
client: null,
|
||||||
|
req: { type: "req", id: "req-1", method: "tools.catalog" },
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("tools.catalog handler", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
pluginToolMetaState.clear();
|
||||||
|
pluginToolMetaState.set("voice_call", { pluginId: "voice-call", optional: true });
|
||||||
|
pluginToolMetaState.set("matrix_room", { pluginId: "matrix", optional: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid params", async () => {
|
||||||
|
const { respond, invoke } = createInvokeParams({ extra: true });
|
||||||
|
await invoke();
|
||||||
|
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||||
|
expect(call?.[0]).toBe(false);
|
||||||
|
expect(call?.[2]?.code).toBe(ErrorCodes.INVALID_REQUEST);
|
||||||
|
expect(call?.[2]?.message).toContain("invalid tools.catalog params");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown agent ids", async () => {
|
||||||
|
const { respond, invoke } = createInvokeParams({ agentId: "unknown-agent" });
|
||||||
|
await invoke();
|
||||||
|
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||||
|
expect(call?.[0]).toBe(false);
|
||||||
|
expect(call?.[2]?.code).toBe(ErrorCodes.INVALID_REQUEST);
|
||||||
|
expect(call?.[2]?.message).toContain("unknown agent id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns core groups including tts and excludes plugins when includePlugins=false", async () => {
|
||||||
|
const { respond, invoke } = createInvokeParams({ includePlugins: false });
|
||||||
|
await invoke();
|
||||||
|
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||||
|
expect(call?.[0]).toBe(true);
|
||||||
|
const payload = call?.[1] as
|
||||||
|
| {
|
||||||
|
agentId: string;
|
||||||
|
groups: Array<{
|
||||||
|
id: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
tools: Array<{ id: string; source: "core" | "plugin" }>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
expect(payload?.agentId).toBe("main");
|
||||||
|
expect(payload?.groups.some((group) => group.source === "plugin")).toBe(false);
|
||||||
|
const media = payload?.groups.find((group) => group.id === "media");
|
||||||
|
expect(media?.tools.some((tool) => tool.id === "tts" && tool.source === "core")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes plugin groups with plugin metadata", async () => {
|
||||||
|
const { respond, invoke } = createInvokeParams({});
|
||||||
|
await invoke();
|
||||||
|
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||||
|
expect(call?.[0]).toBe(true);
|
||||||
|
const payload = call?.[1] as
|
||||||
|
| {
|
||||||
|
groups: Array<{
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
tools: Array<{
|
||||||
|
id: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
optional?: boolean;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const pluginGroups = (payload?.groups ?? []).filter((group) => group.source === "plugin");
|
||||||
|
expect(pluginGroups.length).toBeGreaterThan(0);
|
||||||
|
const voiceCall = pluginGroups
|
||||||
|
.flatMap((group) => group.tools)
|
||||||
|
.find((tool) => tool.id === "voice_call");
|
||||||
|
expect(voiceCall).toMatchObject({
|
||||||
|
source: "plugin",
|
||||||
|
pluginId: "voice-call",
|
||||||
|
optional: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
165
src/gateway/server-methods/tools-catalog.ts
Normal file
165
src/gateway/server-methods/tools-catalog.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import {
|
||||||
|
listAgentIds,
|
||||||
|
resolveAgentDir,
|
||||||
|
resolveAgentWorkspaceDir,
|
||||||
|
resolveDefaultAgentId,
|
||||||
|
} from "../../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
|
listCoreToolSections,
|
||||||
|
PROFILE_OPTIONS,
|
||||||
|
resolveCoreToolProfiles,
|
||||||
|
} from "../../agents/tool-catalog.js";
|
||||||
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { getPluginToolMeta, resolvePluginTools } from "../../plugins/tools.js";
|
||||||
|
import {
|
||||||
|
ErrorCodes,
|
||||||
|
errorShape,
|
||||||
|
formatValidationErrors,
|
||||||
|
validateToolsCatalogParams,
|
||||||
|
} from "../protocol/index.js";
|
||||||
|
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||||
|
|
||||||
|
type ToolCatalogEntry = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
optional?: boolean;
|
||||||
|
defaultProfiles: Array<"minimal" | "coding" | "messaging" | "full">;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolCatalogGroup = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
tools: ToolCatalogEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveAgentIdOrRespondError(rawAgentId: unknown, respond: RespondFn) {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const knownAgents = listAgentIds(cfg);
|
||||||
|
const requestedAgentId = typeof rawAgentId === "string" ? rawAgentId.trim() : "";
|
||||||
|
const agentId = requestedAgentId || resolveDefaultAgentId(cfg);
|
||||||
|
if (requestedAgentId && !knownAgents.includes(agentId)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `unknown agent id "${requestedAgentId}"`),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { cfg, agentId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCoreGroups(): ToolCatalogGroup[] {
|
||||||
|
return listCoreToolSections().map((section) => ({
|
||||||
|
id: section.id,
|
||||||
|
label: section.label,
|
||||||
|
source: "core",
|
||||||
|
tools: section.tools.map((tool) => ({
|
||||||
|
id: tool.id,
|
||||||
|
label: tool.label,
|
||||||
|
description: tool.description,
|
||||||
|
source: "core",
|
||||||
|
defaultProfiles: resolveCoreToolProfiles(tool.id),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginGroups(params: {
|
||||||
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
|
agentId: string;
|
||||||
|
existingToolNames: Set<string>;
|
||||||
|
}): ToolCatalogGroup[] {
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
||||||
|
const agentDir = resolveAgentDir(params.cfg, params.agentId);
|
||||||
|
const pluginTools = resolvePluginTools({
|
||||||
|
context: {
|
||||||
|
config: params.cfg,
|
||||||
|
workspaceDir,
|
||||||
|
agentDir,
|
||||||
|
agentId: params.agentId,
|
||||||
|
},
|
||||||
|
existingToolNames: params.existingToolNames,
|
||||||
|
toolAllowlist: ["group:plugins"],
|
||||||
|
});
|
||||||
|
const groups = new Map<string, ToolCatalogGroup>();
|
||||||
|
for (const tool of pluginTools) {
|
||||||
|
const meta = getPluginToolMeta(tool);
|
||||||
|
const pluginId = meta?.pluginId ?? "plugin";
|
||||||
|
const groupId = `plugin:${pluginId}`;
|
||||||
|
const existing =
|
||||||
|
groups.get(groupId) ??
|
||||||
|
({
|
||||||
|
id: groupId,
|
||||||
|
label: pluginId,
|
||||||
|
source: "plugin",
|
||||||
|
pluginId,
|
||||||
|
tools: [],
|
||||||
|
} as ToolCatalogGroup);
|
||||||
|
existing.tools.push({
|
||||||
|
id: tool.name,
|
||||||
|
label: typeof tool.label === "string" && tool.label.trim() ? tool.label.trim() : tool.name,
|
||||||
|
description:
|
||||||
|
typeof tool.description === "string" && tool.description.trim()
|
||||||
|
? tool.description.trim()
|
||||||
|
: "Plugin tool",
|
||||||
|
source: "plugin",
|
||||||
|
pluginId,
|
||||||
|
optional: meta?.optional,
|
||||||
|
defaultProfiles: [],
|
||||||
|
});
|
||||||
|
groups.set(groupId, existing);
|
||||||
|
}
|
||||||
|
return [...groups.values()]
|
||||||
|
.map((group) => ({
|
||||||
|
...group,
|
||||||
|
tools: group.tools.toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
}))
|
||||||
|
.toSorted((a, b) => a.label.localeCompare(b.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toolsCatalogHandlers: GatewayRequestHandlers = {
|
||||||
|
"tools.catalog": ({ params, respond }) => {
|
||||||
|
if (!validateToolsCatalogParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid tools.catalog params: ${formatValidationErrors(validateToolsCatalogParams.errors)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resolved = resolveAgentIdOrRespondError(params.agentId, respond);
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const includePlugins = params.includePlugins !== false;
|
||||||
|
const groups = buildCoreGroups();
|
||||||
|
if (includePlugins) {
|
||||||
|
const existingToolNames = new Set(
|
||||||
|
groups.flatMap((group) => group.tools.map((tool) => tool.id)),
|
||||||
|
);
|
||||||
|
groups.push(
|
||||||
|
...buildPluginGroups({
|
||||||
|
cfg: resolved.cfg,
|
||||||
|
agentId: resolved.agentId,
|
||||||
|
existingToolNames,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
agentId: resolved.agentId,
|
||||||
|
profiles: PROFILE_OPTIONS.map((profile) => ({ id: profile.id, label: profile.label })),
|
||||||
|
groups,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
46
src/gateway/server.tools-catalog.test.ts
Normal file
46
src/gateway/server.tools-catalog.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { connectOk, installGatewayTestHooks, rpcReq } from "./test-helpers.js";
|
||||||
|
import { withServer } from "./test-with-server.js";
|
||||||
|
|
||||||
|
installGatewayTestHooks({ scope: "suite" });
|
||||||
|
|
||||||
|
describe("gateway tools.catalog", () => {
|
||||||
|
it("returns core catalog data and includes tts", async () => {
|
||||||
|
await withServer(async (ws) => {
|
||||||
|
await connectOk(ws, { token: "secret", scopes: ["operator.read"] });
|
||||||
|
const res = await rpcReq<{
|
||||||
|
agentId?: string;
|
||||||
|
groups?: Array<{
|
||||||
|
id?: string;
|
||||||
|
source?: "core" | "plugin";
|
||||||
|
tools?: Array<{ id?: string; source?: "core" | "plugin" }>;
|
||||||
|
}>;
|
||||||
|
}>(ws, "tools.catalog", {});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.payload?.agentId).toBeTruthy();
|
||||||
|
const mediaGroup = res.payload?.groups?.find((group) => group.id === "media");
|
||||||
|
expect(mediaGroup?.tools?.some((tool) => tool.id === "tts" && tool.source === "core")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports includePlugins=false and rejects unknown agent ids", async () => {
|
||||||
|
await withServer(async (ws) => {
|
||||||
|
await connectOk(ws, { token: "secret", scopes: ["operator.read"] });
|
||||||
|
|
||||||
|
const noPlugins = await rpcReq<{
|
||||||
|
groups?: Array<{ source?: "core" | "plugin" }>;
|
||||||
|
}>(ws, "tools.catalog", { includePlugins: false });
|
||||||
|
expect(noPlugins.ok).toBe(true);
|
||||||
|
expect((noPlugins.payload?.groups ?? []).every((group) => group.source !== "plugin")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unknownAgent = await rpcReq(ws, "tools.catalog", { agentId: "does-not-exist" });
|
||||||
|
expect(unknownAgent.ok).toBe(false);
|
||||||
|
expect(unknownAgent.error?.message ?? "").toContain("unknown agent id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream.ts";
|
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream.ts";
|
||||||
import type { OpenClawApp } from "./app.ts";
|
import type { OpenClawApp } from "./app.ts";
|
||||||
import { shouldReloadHistoryForFinalEvent } from "./chat-event-reload.ts";
|
import { shouldReloadHistoryForFinalEvent } from "./chat-event-reload.ts";
|
||||||
import { loadAgents } from "./controllers/agents.ts";
|
import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts";
|
||||||
import { loadAssistantIdentity } from "./controllers/assistant-identity.ts";
|
import { loadAssistantIdentity } from "./controllers/assistant-identity.ts";
|
||||||
import { loadChatHistory } from "./controllers/chat.ts";
|
import { loadChatHistory } from "./controllers/chat.ts";
|
||||||
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat.ts";
|
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat.ts";
|
||||||
@@ -62,6 +62,9 @@ type GatewayHost = {
|
|||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsList: AgentsListResult | null;
|
agentsList: AgentsListResult | null;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
|
toolsCatalogLoading: boolean;
|
||||||
|
toolsCatalogError: string | null;
|
||||||
|
toolsCatalogResult: import("./types.ts").ToolsCatalogResult | null;
|
||||||
debugHealth: HealthSnapshot | null;
|
debugHealth: HealthSnapshot | null;
|
||||||
assistantName: string;
|
assistantName: string;
|
||||||
assistantAvatar: string | null;
|
assistantAvatar: string | null;
|
||||||
@@ -166,6 +169,7 @@ export function connectGateway(host: GatewayHost) {
|
|||||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||||
void loadAssistantIdentity(host as unknown as OpenClawApp);
|
void loadAssistantIdentity(host as unknown as OpenClawApp);
|
||||||
void loadAgents(host as unknown as OpenClawApp);
|
void loadAgents(host as unknown as OpenClawApp);
|
||||||
|
void loadToolsCatalog(host as unknown as OpenClawApp);
|
||||||
void loadNodes(host as unknown as OpenClawApp, { quiet: true });
|
void loadNodes(host as unknown as OpenClawApp, { quiet: true });
|
||||||
void loadDevices(host as unknown as OpenClawApp, { quiet: true });
|
void loadDevices(host as unknown as OpenClawApp, { quiet: true });
|
||||||
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
|
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { AppViewState } from "./app-view-state.ts";
|
|||||||
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts";
|
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts";
|
||||||
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
||||||
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
||||||
import { loadAgents } from "./controllers/agents.ts";
|
import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts";
|
||||||
import { loadChannels } from "./controllers/channels.ts";
|
import { loadChannels } from "./controllers/channels.ts";
|
||||||
import { loadChatHistory } from "./controllers/chat.ts";
|
import { loadChatHistory } from "./controllers/chat.ts";
|
||||||
import {
|
import {
|
||||||
@@ -528,9 +528,18 @@ export function renderApp(state: AppViewState) {
|
|||||||
agentSkillsReport: state.agentSkillsReport,
|
agentSkillsReport: state.agentSkillsReport,
|
||||||
agentSkillsError: state.agentSkillsError,
|
agentSkillsError: state.agentSkillsError,
|
||||||
agentSkillsAgentId: state.agentSkillsAgentId,
|
agentSkillsAgentId: state.agentSkillsAgentId,
|
||||||
|
toolsCatalogLoading: state.toolsCatalogLoading,
|
||||||
|
toolsCatalogError: state.toolsCatalogError,
|
||||||
|
toolsCatalogResult: state.toolsCatalogResult,
|
||||||
skillsFilter: state.skillsFilter,
|
skillsFilter: state.skillsFilter,
|
||||||
onRefresh: async () => {
|
onRefresh: async () => {
|
||||||
await loadAgents(state);
|
await loadAgents(state);
|
||||||
|
const nextSelected =
|
||||||
|
state.agentsSelectedId ??
|
||||||
|
state.agentsList?.defaultId ??
|
||||||
|
state.agentsList?.agents?.[0]?.id ??
|
||||||
|
null;
|
||||||
|
await loadToolsCatalog(state, nextSelected);
|
||||||
const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||||
if (agentIds.length > 0) {
|
if (agentIds.length > 0) {
|
||||||
void loadAgentIdentities(state, agentIds);
|
void loadAgentIdentities(state, agentIds);
|
||||||
@@ -551,6 +560,9 @@ export function renderApp(state: AppViewState) {
|
|||||||
state.agentSkillsError = null;
|
state.agentSkillsError = null;
|
||||||
state.agentSkillsAgentId = null;
|
state.agentSkillsAgentId = null;
|
||||||
void loadAgentIdentity(state, agentId);
|
void loadAgentIdentity(state, agentId);
|
||||||
|
if (state.agentsPanel === "tools") {
|
||||||
|
void loadToolsCatalog(state, agentId);
|
||||||
|
}
|
||||||
if (state.agentsPanel === "files") {
|
if (state.agentsPanel === "files") {
|
||||||
void loadAgentFiles(state, agentId);
|
void loadAgentFiles(state, agentId);
|
||||||
}
|
}
|
||||||
@@ -570,6 +582,9 @@ export function renderApp(state: AppViewState) {
|
|||||||
void loadAgentFiles(state, resolvedAgentId);
|
void loadAgentFiles(state, resolvedAgentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (panel === "tools") {
|
||||||
|
void loadToolsCatalog(state, resolvedAgentId);
|
||||||
|
}
|
||||||
if (panel === "skills") {
|
if (panel === "skills") {
|
||||||
if (resolvedAgentId) {
|
if (resolvedAgentId) {
|
||||||
void loadAgentSkills(state, resolvedAgentId);
|
void loadAgentSkills(state, resolvedAgentId);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll.ts";
|
|||||||
import type { OpenClawApp } from "./app.ts";
|
import type { OpenClawApp } from "./app.ts";
|
||||||
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
||||||
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
||||||
import { loadAgents } from "./controllers/agents.ts";
|
import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts";
|
||||||
import { loadChannels } from "./controllers/channels.ts";
|
import { loadChannels } from "./controllers/channels.ts";
|
||||||
import { loadConfig, loadConfigSchema } from "./controllers/config.ts";
|
import { loadConfig, loadConfigSchema } from "./controllers/config.ts";
|
||||||
import {
|
import {
|
||||||
@@ -204,6 +204,7 @@ export async function refreshActiveTab(host: SettingsHost) {
|
|||||||
}
|
}
|
||||||
if (host.tab === "agents") {
|
if (host.tab === "agents") {
|
||||||
await loadAgents(host as unknown as OpenClawApp);
|
await loadAgents(host as unknown as OpenClawApp);
|
||||||
|
await loadToolsCatalog(host as unknown as OpenClawApp);
|
||||||
await loadConfig(host as unknown as OpenClawApp);
|
await loadConfig(host as unknown as OpenClawApp);
|
||||||
const agentIds = host.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
const agentIds = host.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||||
if (agentIds.length > 0) {
|
if (agentIds.length > 0) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type {
|
|||||||
SessionUsageTimeSeries,
|
SessionUsageTimeSeries,
|
||||||
SessionsListResult,
|
SessionsListResult,
|
||||||
SkillStatusReport,
|
SkillStatusReport,
|
||||||
|
ToolsCatalogResult,
|
||||||
StatusSummary,
|
StatusSummary,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types.ts";
|
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types.ts";
|
||||||
@@ -137,6 +138,9 @@ export type AppViewState = {
|
|||||||
agentsList: AgentsListResult | null;
|
agentsList: AgentsListResult | null;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
agentsSelectedId: string | null;
|
agentsSelectedId: string | null;
|
||||||
|
toolsCatalogLoading: boolean;
|
||||||
|
toolsCatalogError: string | null;
|
||||||
|
toolsCatalogResult: ToolsCatalogResult | null;
|
||||||
agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
||||||
agentFilesLoading: boolean;
|
agentFilesLoading: boolean;
|
||||||
agentFilesError: string | null;
|
agentFilesError: string | null;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import type {
|
|||||||
ChannelsStatusSnapshot,
|
ChannelsStatusSnapshot,
|
||||||
SessionsListResult,
|
SessionsListResult,
|
||||||
SkillStatusReport,
|
SkillStatusReport,
|
||||||
|
ToolsCatalogResult,
|
||||||
StatusSummary,
|
StatusSummary,
|
||||||
NostrProfile,
|
NostrProfile,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
@@ -217,6 +218,9 @@ export class OpenClawApp extends LitElement {
|
|||||||
@state() agentsList: AgentsListResult | null = null;
|
@state() agentsList: AgentsListResult | null = null;
|
||||||
@state() agentsError: string | null = null;
|
@state() agentsError: string | null = null;
|
||||||
@state() agentsSelectedId: string | null = null;
|
@state() agentsSelectedId: string | null = null;
|
||||||
|
@state() toolsCatalogLoading = false;
|
||||||
|
@state() toolsCatalogError: string | null = null;
|
||||||
|
@state() toolsCatalogResult: ToolsCatalogResult | null = null;
|
||||||
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
|
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
|
||||||
"overview";
|
"overview";
|
||||||
@state() agentFilesLoading = false;
|
@state() agentFilesLoading = false;
|
||||||
|
|||||||
61
ui/src/ui/controllers/agents.test.ts
Normal file
61
ui/src/ui/controllers/agents.test.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { loadToolsCatalog } from "./agents.ts";
|
||||||
|
import type { AgentsState } from "./agents.ts";
|
||||||
|
|
||||||
|
function createState(): { state: AgentsState; request: ReturnType<typeof vi.fn> } {
|
||||||
|
const request = vi.fn();
|
||||||
|
const state: AgentsState = {
|
||||||
|
client: {
|
||||||
|
request,
|
||||||
|
} as unknown as AgentsState["client"],
|
||||||
|
connected: true,
|
||||||
|
agentsLoading: false,
|
||||||
|
agentsError: null,
|
||||||
|
agentsList: null,
|
||||||
|
agentsSelectedId: "main",
|
||||||
|
toolsCatalogLoading: false,
|
||||||
|
toolsCatalogError: null,
|
||||||
|
toolsCatalogResult: null,
|
||||||
|
};
|
||||||
|
return { state, request };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("loadToolsCatalog", () => {
|
||||||
|
it("loads catalog and stores result", async () => {
|
||||||
|
const { state, request } = createState();
|
||||||
|
const payload = {
|
||||||
|
agentId: "main",
|
||||||
|
profiles: [{ id: "full", label: "Full" }],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: "media",
|
||||||
|
label: "Media",
|
||||||
|
source: "core",
|
||||||
|
tools: [{ id: "tts", label: "tts", description: "Text-to-speech", source: "core" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
request.mockResolvedValue(payload);
|
||||||
|
|
||||||
|
await loadToolsCatalog(state, "main");
|
||||||
|
|
||||||
|
expect(request).toHaveBeenCalledWith("tools.catalog", {
|
||||||
|
agentId: "main",
|
||||||
|
includePlugins: true,
|
||||||
|
});
|
||||||
|
expect(state.toolsCatalogResult).toEqual(payload);
|
||||||
|
expect(state.toolsCatalogError).toBeNull();
|
||||||
|
expect(state.toolsCatalogLoading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures request errors for fallback UI handling", async () => {
|
||||||
|
const { state, request } = createState();
|
||||||
|
request.mockRejectedValue(new Error("gateway unavailable"));
|
||||||
|
|
||||||
|
await loadToolsCatalog(state, "main");
|
||||||
|
|
||||||
|
expect(state.toolsCatalogResult).toBeNull();
|
||||||
|
expect(state.toolsCatalogError).toContain("gateway unavailable");
|
||||||
|
expect(state.toolsCatalogLoading).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||||
import type { AgentsListResult } from "../types.ts";
|
import type { AgentsListResult, ToolsCatalogResult } from "../types.ts";
|
||||||
|
|
||||||
export type AgentsState = {
|
export type AgentsState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
@@ -8,6 +8,9 @@ export type AgentsState = {
|
|||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
agentsList: AgentsListResult | null;
|
agentsList: AgentsListResult | null;
|
||||||
agentsSelectedId: string | null;
|
agentsSelectedId: string | null;
|
||||||
|
toolsCatalogLoading: boolean;
|
||||||
|
toolsCatalogError: string | null;
|
||||||
|
toolsCatalogResult: ToolsCatalogResult | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadAgents(state: AgentsState) {
|
export async function loadAgents(state: AgentsState) {
|
||||||
@@ -35,3 +38,27 @@ export async function loadAgents(state: AgentsState) {
|
|||||||
state.agentsLoading = false;
|
state.agentsLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadToolsCatalog(state: AgentsState, agentId?: string | null) {
|
||||||
|
if (!state.client || !state.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.toolsCatalogLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.toolsCatalogLoading = true;
|
||||||
|
state.toolsCatalogError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request<ToolsCatalogResult>("tools.catalog", {
|
||||||
|
agentId: agentId ?? state.agentsSelectedId ?? undefined,
|
||||||
|
includePlugins: true,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
state.toolsCatalogResult = res;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.toolsCatalogError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.toolsCatalogLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -345,6 +345,35 @@ export type AgentsListResult = {
|
|||||||
agents: GatewayAgentRow[];
|
agents: GatewayAgentRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ToolCatalogProfile = {
|
||||||
|
id: "minimal" | "coding" | "messaging" | "full";
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolCatalogEntry = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
optional?: boolean;
|
||||||
|
defaultProfiles: Array<"minimal" | "coding" | "messaging" | "full">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolCatalogGroup = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
source: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
tools: ToolCatalogEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolsCatalogResult = {
|
||||||
|
agentId: string;
|
||||||
|
profiles: ToolCatalogProfile[];
|
||||||
|
groups: ToolCatalogGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AgentIdentityResult = {
|
export type AgentIdentityResult = {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
102
ui/src/ui/views/agents-panels-tools-skills.browser.test.ts
Normal file
102
ui/src/ui/views/agents-panels-tools-skills.browser.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { render } from "lit";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { renderAgentTools } from "./agents-panels-tools-skills.ts";
|
||||||
|
|
||||||
|
function createBaseParams(overrides: Partial<Parameters<typeof renderAgentTools>[0]> = {}) {
|
||||||
|
return {
|
||||||
|
agentId: "main",
|
||||||
|
configForm: {
|
||||||
|
agents: {
|
||||||
|
list: [{ id: "main", tools: { profile: "full" } }],
|
||||||
|
},
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
configLoading: false,
|
||||||
|
configSaving: false,
|
||||||
|
configDirty: false,
|
||||||
|
toolsCatalogLoading: false,
|
||||||
|
toolsCatalogError: null,
|
||||||
|
toolsCatalogResult: null,
|
||||||
|
onProfileChange: () => undefined,
|
||||||
|
onOverridesChange: () => undefined,
|
||||||
|
onConfigReload: () => undefined,
|
||||||
|
onConfigSave: () => undefined,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("agents tools panel (browser)", () => {
|
||||||
|
it("renders per-tool provenance badges and optional marker", async () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderAgentTools(
|
||||||
|
createBaseParams({
|
||||||
|
toolsCatalogResult: {
|
||||||
|
agentId: "main",
|
||||||
|
profiles: [
|
||||||
|
{ id: "minimal", label: "Minimal" },
|
||||||
|
{ id: "coding", label: "Coding" },
|
||||||
|
{ id: "messaging", label: "Messaging" },
|
||||||
|
{ id: "full", label: "Full" },
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: "media",
|
||||||
|
label: "Media",
|
||||||
|
source: "core",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
id: "tts",
|
||||||
|
label: "tts",
|
||||||
|
description: "Text-to-speech conversion",
|
||||||
|
source: "core",
|
||||||
|
defaultProfiles: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plugin:voice-call",
|
||||||
|
label: "voice-call",
|
||||||
|
source: "plugin",
|
||||||
|
pluginId: "voice-call",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
id: "voice_call",
|
||||||
|
label: "voice_call",
|
||||||
|
description: "Voice call tool",
|
||||||
|
source: "plugin",
|
||||||
|
pluginId: "voice-call",
|
||||||
|
optional: true,
|
||||||
|
defaultProfiles: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const text = container.textContent ?? "";
|
||||||
|
expect(text).toContain("core");
|
||||||
|
expect(text).toContain("plugin:voice-call");
|
||||||
|
expect(text).toContain("optional");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows fallback warning when runtime catalog fails", async () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderAgentTools(
|
||||||
|
createBaseParams({
|
||||||
|
toolsCatalogError: "unavailable",
|
||||||
|
toolsCatalogResult: null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(container.textContent ?? "").toContain("Could not load runtime tool catalog");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { normalizeToolName } from "../../../../src/agents/tool-policy-shared.js";
|
import { normalizeToolName } from "../../../../src/agents/tool-policy-shared.js";
|
||||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
import type { SkillStatusEntry, SkillStatusReport, ToolsCatalogResult } from "../types.ts";
|
||||||
import {
|
import {
|
||||||
isAllowedByPolicy,
|
isAllowedByPolicy,
|
||||||
matchesList,
|
matchesList,
|
||||||
@@ -23,6 +23,9 @@ export function renderAgentTools(params: {
|
|||||||
configLoading: boolean;
|
configLoading: boolean;
|
||||||
configSaving: boolean;
|
configSaving: boolean;
|
||||||
configDirty: boolean;
|
configDirty: boolean;
|
||||||
|
toolsCatalogLoading: boolean;
|
||||||
|
toolsCatalogError: string | null;
|
||||||
|
toolsCatalogResult: ToolsCatalogResult | null;
|
||||||
onProfileChange: (agentId: string, profile: string | null, clearAllow: boolean) => void;
|
onProfileChange: (agentId: string, profile: string | null, clearAllow: boolean) => void;
|
||||||
onOverridesChange: (agentId: string, alsoAllow: string[], deny: string[]) => void;
|
onOverridesChange: (agentId: string, alsoAllow: string[], deny: string[]) => void;
|
||||||
onConfigReload: () => void;
|
onConfigReload: () => void;
|
||||||
@@ -50,7 +53,17 @@ export function renderAgentTools(params: {
|
|||||||
const basePolicy = hasAgentAllow
|
const basePolicy = hasAgentAllow
|
||||||
? { allow: agentTools.allow ?? [], deny: agentTools.deny ?? [] }
|
? { allow: agentTools.allow ?? [], deny: agentTools.deny ?? [] }
|
||||||
: (resolveToolProfile(profile) ?? undefined);
|
: (resolveToolProfile(profile) ?? undefined);
|
||||||
const toolIds = TOOL_SECTIONS.flatMap((section) => section.tools.map((tool) => tool.id));
|
const sections =
|
||||||
|
params.toolsCatalogResult?.groups?.length &&
|
||||||
|
params.toolsCatalogResult.agentId === params.agentId
|
||||||
|
? params.toolsCatalogResult.groups
|
||||||
|
: TOOL_SECTIONS;
|
||||||
|
const profileOptions =
|
||||||
|
params.toolsCatalogResult?.profiles?.length &&
|
||||||
|
params.toolsCatalogResult.agentId === params.agentId
|
||||||
|
? params.toolsCatalogResult.profiles
|
||||||
|
: PROFILE_OPTIONS;
|
||||||
|
const toolIds = sections.flatMap((section) => section.tools.map((tool) => tool.id));
|
||||||
|
|
||||||
const resolveAllowed = (toolId: string) => {
|
const resolveAllowed = (toolId: string) => {
|
||||||
const baseAllowed = isAllowedByPolicy(toolId, basePolicy);
|
const baseAllowed = isAllowedByPolicy(toolId, basePolicy);
|
||||||
@@ -139,6 +152,15 @@ export function renderAgentTools(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
params.toolsCatalogError
|
||||||
|
? html`
|
||||||
|
<div class="callout warn" style="margin-top: 12px">
|
||||||
|
Could not load runtime tool catalog. Showing fallback list.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
${
|
${
|
||||||
!params.configForm
|
!params.configForm
|
||||||
? html`
|
? html`
|
||||||
@@ -191,7 +213,7 @@ export function renderAgentTools(params: {
|
|||||||
<div class="agent-tools-presets" style="margin-top: 16px;">
|
<div class="agent-tools-presets" style="margin-top: 16px;">
|
||||||
<div class="label">Quick Presets</div>
|
<div class="label">Quick Presets</div>
|
||||||
<div class="agent-tools-buttons">
|
<div class="agent-tools-buttons">
|
||||||
${PROFILE_OPTIONS.map(
|
${profileOptions.map(
|
||||||
(option) => html`
|
(option) => html`
|
||||||
<button
|
<button
|
||||||
class="btn btn--sm ${profile === option.id ? "active" : ""}"
|
class="btn btn--sm ${profile === option.id ? "active" : ""}"
|
||||||
@@ -213,18 +235,49 @@ export function renderAgentTools(params: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="agent-tools-grid" style="margin-top: 20px;">
|
<div class="agent-tools-grid" style="margin-top: 20px;">
|
||||||
${TOOL_SECTIONS.map(
|
${sections.map(
|
||||||
(section) =>
|
(section) =>
|
||||||
html`
|
html`
|
||||||
<div class="agent-tools-section">
|
<div class="agent-tools-section">
|
||||||
<div class="agent-tools-header">${section.label}</div>
|
<div class="agent-tools-header">
|
||||||
|
${section.label}
|
||||||
|
${
|
||||||
|
"source" in section && section.source === "plugin"
|
||||||
|
? html`
|
||||||
|
<span class="mono" style="margin-left: 6px">plugin</span>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="agent-tools-list">
|
<div class="agent-tools-list">
|
||||||
${section.tools.map((tool) => {
|
${section.tools.map((tool) => {
|
||||||
const { allowed } = resolveAllowed(tool.id);
|
const { allowed } = resolveAllowed(tool.id);
|
||||||
|
const catalogTool = tool as {
|
||||||
|
source?: "core" | "plugin";
|
||||||
|
pluginId?: string;
|
||||||
|
optional?: boolean;
|
||||||
|
};
|
||||||
|
const source =
|
||||||
|
catalogTool.source === "plugin"
|
||||||
|
? catalogTool.pluginId
|
||||||
|
? `plugin:${catalogTool.pluginId}`
|
||||||
|
: "plugin"
|
||||||
|
: "core";
|
||||||
|
const isOptional = catalogTool.optional === true;
|
||||||
return html`
|
return html`
|
||||||
<div class="agent-tool-row">
|
<div class="agent-tool-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="agent-tool-title mono">${tool.label}</div>
|
<div class="agent-tool-title mono">
|
||||||
|
${tool.label}
|
||||||
|
<span class="mono" style="margin-left: 8px; opacity: 0.8;">${source}</span>
|
||||||
|
${
|
||||||
|
isOptional
|
||||||
|
? html`
|
||||||
|
<span class="mono" style="margin-left: 6px; opacity: 0.8">optional</span>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="agent-tool-sub">${tool.description}</div>
|
<div class="agent-tool-sub">${tool.description}</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="cfg-toggle">
|
<label class="cfg-toggle">
|
||||||
@@ -245,6 +298,13 @@ export function renderAgentTools(params: {
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
${
|
||||||
|
params.toolsCatalogLoading
|
||||||
|
? html`
|
||||||
|
<div class="card-sub" style="margin-top: 10px">Refreshing tool catalog…</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
import {
|
||||||
|
listCoreToolSections,
|
||||||
|
PROFILE_OPTIONS as TOOL_PROFILE_OPTIONS,
|
||||||
|
} from "../../../../src/agents/tool-catalog.js";
|
||||||
import {
|
import {
|
||||||
expandToolGroups,
|
expandToolGroups,
|
||||||
normalizeToolName,
|
normalizeToolName,
|
||||||
@@ -6,96 +10,9 @@ import {
|
|||||||
} from "../../../../src/agents/tool-policy-shared.js";
|
} from "../../../../src/agents/tool-policy-shared.js";
|
||||||
import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts";
|
import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts";
|
||||||
|
|
||||||
export const TOOL_SECTIONS = [
|
export const TOOL_SECTIONS = listCoreToolSections();
|
||||||
{
|
|
||||||
id: "fs",
|
|
||||||
label: "Files",
|
|
||||||
tools: [
|
|
||||||
{ id: "read", label: "read", description: "Read file contents" },
|
|
||||||
{ id: "write", label: "write", description: "Create or overwrite files" },
|
|
||||||
{ id: "edit", label: "edit", description: "Make precise edits" },
|
|
||||||
{ id: "apply_patch", label: "apply_patch", description: "Patch files (OpenAI)" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "runtime",
|
|
||||||
label: "Runtime",
|
|
||||||
tools: [
|
|
||||||
{ id: "exec", label: "exec", description: "Run shell commands" },
|
|
||||||
{ id: "process", label: "process", description: "Manage background processes" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "web",
|
|
||||||
label: "Web",
|
|
||||||
tools: [
|
|
||||||
{ id: "web_search", label: "web_search", description: "Search the web" },
|
|
||||||
{ id: "web_fetch", label: "web_fetch", description: "Fetch web content" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "memory",
|
|
||||||
label: "Memory",
|
|
||||||
tools: [
|
|
||||||
{ id: "memory_search", label: "memory_search", description: "Semantic search" },
|
|
||||||
{ id: "memory_get", label: "memory_get", description: "Read memory files" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sessions",
|
|
||||||
label: "Sessions",
|
|
||||||
tools: [
|
|
||||||
{ id: "sessions_list", label: "sessions_list", description: "List sessions" },
|
|
||||||
{ id: "sessions_history", label: "sessions_history", description: "Session history" },
|
|
||||||
{ id: "sessions_send", label: "sessions_send", description: "Send to session" },
|
|
||||||
{ id: "sessions_spawn", label: "sessions_spawn", description: "Spawn sub-agent" },
|
|
||||||
{ id: "session_status", label: "session_status", description: "Session status" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ui",
|
|
||||||
label: "UI",
|
|
||||||
tools: [
|
|
||||||
{ id: "browser", label: "browser", description: "Control web browser" },
|
|
||||||
{ id: "canvas", label: "canvas", description: "Control canvases" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "messaging",
|
|
||||||
label: "Messaging",
|
|
||||||
tools: [{ id: "message", label: "message", description: "Send messages" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "automation",
|
|
||||||
label: "Automation",
|
|
||||||
tools: [
|
|
||||||
{ id: "cron", label: "cron", description: "Schedule tasks" },
|
|
||||||
{ id: "gateway", label: "gateway", description: "Gateway control" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "nodes",
|
|
||||||
label: "Nodes",
|
|
||||||
tools: [{ id: "nodes", label: "nodes", description: "Nodes + devices" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "agents",
|
|
||||||
label: "Agents",
|
|
||||||
tools: [{ id: "agents_list", label: "agents_list", description: "List agents" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "media",
|
|
||||||
label: "Media",
|
|
||||||
tools: [{ id: "image", label: "image", description: "Image understanding" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PROFILE_OPTIONS = [
|
export const PROFILE_OPTIONS = TOOL_PROFILE_OPTIONS;
|
||||||
{ id: "minimal", label: "Minimal" },
|
|
||||||
{ id: "coding", label: "Coding" },
|
|
||||||
{ id: "messaging", label: "Messaging" },
|
|
||||||
{ id: "full", label: "Full" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type ToolPolicy = {
|
type ToolPolicy = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
CronJob,
|
CronJob,
|
||||||
CronStatus,
|
CronStatus,
|
||||||
SkillStatusReport,
|
SkillStatusReport,
|
||||||
|
ToolsCatalogResult,
|
||||||
} from "../types.ts";
|
} from "../types.ts";
|
||||||
import {
|
import {
|
||||||
renderAgentFiles,
|
renderAgentFiles,
|
||||||
@@ -62,6 +63,9 @@ export type AgentsProps = {
|
|||||||
agentSkillsReport: SkillStatusReport | null;
|
agentSkillsReport: SkillStatusReport | null;
|
||||||
agentSkillsError: string | null;
|
agentSkillsError: string | null;
|
||||||
agentSkillsAgentId: string | null;
|
agentSkillsAgentId: string | null;
|
||||||
|
toolsCatalogLoading: boolean;
|
||||||
|
toolsCatalogError: string | null;
|
||||||
|
toolsCatalogResult: ToolsCatalogResult | null;
|
||||||
skillsFilter: string;
|
skillsFilter: string;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onSelectAgent: (agentId: string) => void;
|
onSelectAgent: (agentId: string) => void;
|
||||||
@@ -210,6 +214,9 @@ export function renderAgents(props: AgentsProps) {
|
|||||||
configLoading: props.configLoading,
|
configLoading: props.configLoading,
|
||||||
configSaving: props.configSaving,
|
configSaving: props.configSaving,
|
||||||
configDirty: props.configDirty,
|
configDirty: props.configDirty,
|
||||||
|
toolsCatalogLoading: props.toolsCatalogLoading,
|
||||||
|
toolsCatalogError: props.toolsCatalogError,
|
||||||
|
toolsCatalogResult: props.toolsCatalogResult,
|
||||||
onProfileChange: props.onToolsProfileChange,
|
onProfileChange: props.onToolsProfileChange,
|
||||||
onOverridesChange: props.onToolsOverridesChange,
|
onOverridesChange: props.onToolsOverridesChange,
|
||||||
onConfigReload: props.onConfigReload,
|
onConfigReload: props.onConfigReload,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default defineConfig({
|
|||||||
"extensions/**/*.test.ts",
|
"extensions/**/*.test.ts",
|
||||||
"test/**/*.test.ts",
|
"test/**/*.test.ts",
|
||||||
"ui/src/ui/views/usage-render-details.test.ts",
|
"ui/src/ui/views/usage-render-details.test.ts",
|
||||||
|
"ui/src/ui/controllers/agents.test.ts",
|
||||||
],
|
],
|
||||||
setupFiles: ["test/setup.ts"],
|
setupFiles: ["test/setup.ts"],
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|||||||
Reference in New Issue
Block a user