Skip to content

Commit d5ef6f4

Browse files
lulululu-debughsluoyz
authored andcommitted
feat: add tool permission policy management (#2464)
1 parent 485a90a commit d5ef6f4

11 files changed

Lines changed: 986 additions & 1 deletion

File tree

controllers/tool_policy.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright 2026 The OpenAgent Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controllers
16+
17+
import (
18+
"encoding/json"
19+
20+
"github.com/beego/beego/utils/pagination"
21+
"github.com/the-open-agent/openagent/object"
22+
"github.com/the-open-agent/openagent/util"
23+
)
24+
25+
// GetToolPolicies
26+
// @Title GetToolPolicies
27+
// @Tag ToolPolicy API
28+
// @Description get tool policies
29+
// @Success 200 {array} object.ToolPolicy The Response object
30+
// @router /get-tool-policies [get]
31+
func (c *ApiController) GetToolPolicies() {
32+
owner := "admin"
33+
limit := c.Input().Get("pageSize")
34+
page := c.Input().Get("p")
35+
field := c.Input().Get("field")
36+
value := c.Input().Get("value")
37+
sortField := c.Input().Get("sortField")
38+
sortOrder := c.Input().Get("sortOrder")
39+
40+
if !c.RequireAdmin() {
41+
return
42+
}
43+
44+
if limit == "" || page == "" {
45+
policies, err := object.GetToolPolicies(owner)
46+
if err != nil {
47+
c.ResponseError(err.Error())
48+
return
49+
}
50+
c.ResponseOk(policies)
51+
} else {
52+
limit := util.ParseInt(limit)
53+
count, err := object.GetToolPolicyCount(owner, field, value)
54+
if err != nil {
55+
c.ResponseError(err.Error())
56+
return
57+
}
58+
59+
paginator := pagination.SetPaginator(c.Ctx, limit, count)
60+
policies, err := object.GetPaginationToolPolicies(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
61+
if err != nil {
62+
c.ResponseError(err.Error())
63+
return
64+
}
65+
66+
c.ResponseOk(policies, paginator.Nums())
67+
}
68+
}
69+
70+
// GetToolPolicy
71+
// @Title GetToolPolicy
72+
// @Tag ToolPolicy API
73+
// @Description get tool policy
74+
// @Param id query string true "The id of tool policy"
75+
// @Success 200 {object} object.ToolPolicy The Response object
76+
// @router /get-tool-policy [get]
77+
func (c *ApiController) GetToolPolicy() {
78+
if !c.RequireAdmin() {
79+
return
80+
}
81+
id := c.Input().Get("id")
82+
83+
p, err := object.GetToolPolicy(id)
84+
if err != nil {
85+
c.ResponseError(err.Error())
86+
return
87+
}
88+
89+
c.ResponseOk(p)
90+
}
91+
92+
// UpdateToolPolicy
93+
// @Title UpdateToolPolicy
94+
// @Tag ToolPolicy API
95+
// @Description update tool policy
96+
// @Param id query string true "The id (owner/name) of the tool policy"
97+
// @Param body body object.ToolPolicy true "The details of the tool policy"
98+
// @Success 200 {object} controllers.Response The Response object
99+
// @router /update-tool-policy [post]
100+
func (c *ApiController) UpdateToolPolicy() {
101+
if !c.RequireAdmin() {
102+
return
103+
}
104+
id := c.Input().Get("id")
105+
106+
var p object.ToolPolicy
107+
err := json.Unmarshal(c.Ctx.Input.RequestBody, &p)
108+
if err != nil {
109+
c.ResponseError(err.Error())
110+
return
111+
}
112+
113+
success, err := object.UpdateToolPolicy(id, &p)
114+
if err != nil {
115+
c.ResponseError(err.Error())
116+
return
117+
}
118+
119+
c.ResponseOk(success)
120+
}
121+
122+
// AddToolPolicy
123+
// @Title AddToolPolicy
124+
// @Tag ToolPolicy API
125+
// @Description add tool policy
126+
// @Param body body object.ToolPolicy true "The details of the tool policy"
127+
// @Success 200 {object} controllers.Response The Response object
128+
// @router /add-tool-policy [post]
129+
func (c *ApiController) AddToolPolicy() {
130+
if !c.RequireAdmin() {
131+
return
132+
}
133+
134+
var p object.ToolPolicy
135+
err := json.Unmarshal(c.Ctx.Input.RequestBody, &p)
136+
if err != nil {
137+
c.ResponseError(err.Error())
138+
return
139+
}
140+
141+
p.Owner = "admin"
142+
success, err := object.AddToolPolicy(&p)
143+
if err != nil {
144+
c.ResponseError(err.Error())
145+
return
146+
}
147+
148+
c.ResponseOk(success)
149+
}
150+
151+
// DeleteToolPolicy
152+
// @Title DeleteToolPolicy
153+
// @Tag ToolPolicy API
154+
// @Description delete tool policy
155+
// @Param body body object.ToolPolicy true "The details of the tool policy"
156+
// @Success 200 {object} controllers.Response The Response object
157+
// @router /delete-tool-policy [post]
158+
func (c *ApiController) DeleteToolPolicy() {
159+
if !c.RequireAdmin() {
160+
return
161+
}
162+
163+
var p object.ToolPolicy
164+
err := json.Unmarshal(c.Ctx.Input.RequestBody, &p)
165+
if err != nil {
166+
c.ResponseError(err.Error())
167+
return
168+
}
169+
170+
success, err := object.DeleteToolPolicy(&p)
171+
if err != nil {
172+
c.ResponseError(err.Error())
173+
return
174+
}
175+
176+
c.ResponseOk(success)
177+
}

object/adapter.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,4 +437,9 @@ func (a *Adapter) createTable() {
437437
if err != nil {
438438
panic(err)
439439
}
440+
441+
err = a.engine.Sync2(new(ToolPolicy))
442+
if err != nil {
443+
panic(err)
444+
}
440445
}

object/tool_policy.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2026 The OpenAgent Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package object
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/the-open-agent/openagent/guard"
22+
"github.com/the-open-agent/openagent/util"
23+
"xorm.io/core"
24+
)
25+
26+
// ToolPolicy is a single tool-permission rule. It is the DB-backed source of
27+
// the loosely-coupled guard engine: rows are translated into guard.Rule at
28+
// enforcement time. Rules are scoped to a store (agent); an empty or "*" Store
29+
// applies the rule to every store owned by the same owner.
30+
type ToolPolicy struct {
31+
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
32+
Name string `xorm:"varchar(100) notnull pk" json:"name"`
33+
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
34+
35+
DisplayName string `xorm:"varchar(100)" json:"displayName"`
36+
Store string `xorm:"varchar(100) index" json:"store"`
37+
38+
// Match patterns; "" or "*" means "any". Tool/Resource accept globs (*, ?).
39+
Subject string `xorm:"varchar(100)" json:"subject"`
40+
Tool string `xorm:"varchar(200)" json:"tool"`
41+
Category string `xorm:"varchar(100)" json:"category"`
42+
Resource string `xorm:"varchar(500)" json:"resource"`
43+
44+
// Effect is one of "allow" / "ask" / "deny".
45+
Effect string `xorm:"varchar(100)" json:"effect"`
46+
Priority int `json:"priority"`
47+
48+
State string `xorm:"varchar(100)" json:"state"`
49+
}
50+
51+
// normalize canonicalizes and validates a policy before it is persisted, so the
52+
// DB never holds an ambiguous effect/state. Effect is lowercased and MUST be one
53+
// of allow/ask/deny (empty or unknown is rejected — the engine treats unknowns
54+
// as deny, so a mislabeled rule must not slip in silently). An empty state
55+
// defaults to "Active".
56+
func (p *ToolPolicy) normalize() error {
57+
p.Effect = strings.ToLower(strings.TrimSpace(p.Effect))
58+
switch guard.Effect(p.Effect) {
59+
case guard.EffectAllow, guard.EffectAsk, guard.EffectDeny:
60+
default:
61+
return fmt.Errorf("invalid effect %q: must be allow, ask or deny", p.Effect)
62+
}
63+
if p.State == "" {
64+
p.State = "Active"
65+
}
66+
if p.State != "Active" && p.State != "Disabled" {
67+
return fmt.Errorf("invalid state %q: must be Active or Disabled", p.State)
68+
}
69+
return nil
70+
}
71+
72+
func GetToolPolicies(owner string) ([]*ToolPolicy, error) {
73+
policies := []*ToolPolicy{}
74+
err := adapter.engine.Desc("priority").Desc("created_time").Find(&policies, &ToolPolicy{Owner: owner})
75+
return policies, err
76+
}
77+
78+
func getToolPolicy(owner string, name string) (*ToolPolicy, error) {
79+
p := ToolPolicy{Owner: owner, Name: name}
80+
existed, err := adapter.engine.Get(&p)
81+
if err != nil {
82+
return &p, err
83+
}
84+
if existed {
85+
return &p, nil
86+
}
87+
return nil, nil
88+
}
89+
90+
func GetToolPolicy(id string) (*ToolPolicy, error) {
91+
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
92+
if err != nil {
93+
return nil, err
94+
}
95+
return getToolPolicy(owner, name)
96+
}
97+
98+
func GetToolPolicyCount(owner, field, value string) (int64, error) {
99+
session := GetDbSession(owner, -1, -1, field, value, "", "")
100+
return session.Count(&ToolPolicy{})
101+
}
102+
103+
func GetPaginationToolPolicies(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*ToolPolicy, error) {
104+
policies := []*ToolPolicy{}
105+
session := GetDbSession(owner, offset, limit, field, value, sortField, sortOrder)
106+
err := session.Find(&policies)
107+
return policies, err
108+
}
109+
110+
func UpdateToolPolicy(id string, p *ToolPolicy) (bool, error) {
111+
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
112+
if err != nil {
113+
return false, err
114+
}
115+
policyDb, err := getToolPolicy(owner, name)
116+
if err != nil {
117+
return false, err
118+
}
119+
if p == nil || policyDb == nil {
120+
return false, nil
121+
}
122+
if err := p.normalize(); err != nil {
123+
return false, err
124+
}
125+
126+
_, err = adapter.engine.ID(core.PK{owner, name}).AllCols().Update(p)
127+
if err != nil {
128+
return false, err
129+
}
130+
return true, nil
131+
}
132+
133+
func AddToolPolicy(p *ToolPolicy) (bool, error) {
134+
if err := p.normalize(); err != nil {
135+
return false, err
136+
}
137+
affected, err := adapter.engine.Insert(p)
138+
if err != nil {
139+
return false, err
140+
}
141+
return affected != 0, nil
142+
}
143+
144+
func DeleteToolPolicy(p *ToolPolicy) (bool, error) {
145+
affected, err := adapter.engine.ID(core.PK{p.Owner, p.Name}).Delete(&ToolPolicy{})
146+
if err != nil {
147+
return false, err
148+
}
149+
return affected != 0, nil
150+
}

routers/router.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ func initAPI() {
150150
beego.Router("/api/delete-tool", &controllers.ApiController{}, "POST:DeleteTool")
151151
beego.Router("/api/test-tool", &controllers.ApiController{}, "POST:TestTool")
152152

153+
beego.Router("/api/get-tool-policies", &controllers.ApiController{}, "GET:GetToolPolicies")
154+
beego.Router("/api/get-tool-policy", &controllers.ApiController{}, "GET:GetToolPolicy")
155+
beego.Router("/api/update-tool-policy", &controllers.ApiController{}, "POST:UpdateToolPolicy")
156+
beego.Router("/api/add-tool-policy", &controllers.ApiController{}, "POST:AddToolPolicy")
157+
beego.Router("/api/delete-tool-policy", &controllers.ApiController{}, "POST:DeleteToolPolicy")
158+
153159
beego.Router("/api/get-global-files", &controllers.ApiController{}, "GET:GetGlobalFiles")
154160
beego.Router("/api/get-files", &controllers.ApiController{}, "GET:GetFiles")
155161
beego.Router("/api/get-file", &controllers.ApiController{}, "GET:GetFileMy")

web/src/ManagementPage.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
NotificationOutlined,
4343
OrderedListOutlined,
4444
RocketOutlined,
45+
SafetyCertificateOutlined,
4546
SafetyOutlined,
4647
SettingOutlined,
4748
ShopOutlined,
@@ -75,6 +76,8 @@ import SkillListPage from "./SkillListPage";
7576
import SkillEditPage from "./SkillEditPage";
7677
import ToolListPage from "./ToolListPage";
7778
import ToolEditPage from "./ToolEditPage";
79+
import ToolPolicyListPage from "./ToolPolicyListPage";
80+
import ToolPolicyEditPage from "./ToolPolicyEditPage";
7881
import ServerListPage from "./ServerListPage";
7982
import ServerEditPage from "./ServerEditPage";
8083
import ServerStorePage from "./ServerStorePage";
@@ -117,7 +120,7 @@ const {Header, Footer, Content, Sider} = Layout;
117120
function getMenuParentKey(uri) {
118121
if (!uri) {return null;}
119122
if (uri.includes("/chats") || uri.includes("/messages") || uri.includes("/stores")) {return "/basic";}
120-
if (uri.includes("/providers") || uri.includes("/pipes") || uri.includes("/tools") || uri.includes("/servers")) {return "/connectors";}
123+
if (uri.includes("/providers") || uri.includes("/pipes") || uri.includes("/tools") || uri.includes("/tool-policies") || uri.includes("/servers")) {return "/connectors";}
121124
if (uri.includes("/files") || uri.includes("/vectors") || uri.includes("/resources")) {return "/knowledge-base";}
122125
if (uri.includes("/tasks") || uri.includes("/scales") || uri.includes("/forms")) {return "/multimedia";}
123126
if (uri.includes("/sessions") || uri.includes("/records") || uri.includes("/snapshots") || uri.includes("/notifications")) {return "/logs";}
@@ -478,6 +481,7 @@ function ManagementPage(props) {
478481
Setting.getItem(<Link to="/pipes">{i18next.t("general:Pipes")}</Link>, "/pipes", <MessageOutlined />),
479482
Setting.getItem(<Link to="/skills">{i18next.t("general:Skills")}</Link>, "/skills", <RocketOutlined />),
480483
Setting.getItem(<Link to="/tools">{i18next.t("general:Tools")}</Link>, "/tools", <ToolOutlined />),
484+
Setting.getItem(<Link to="/tool-policies">{i18next.t("toolPolicy:Tool Permissions")}</Link>, "/tool-policies", <SafetyCertificateOutlined />),
481485
Setting.getItem(<Link to="/servers">{i18next.t("general:MCP Servers")}</Link>, "/servers", <ApiOutlined />),
482486
]));
483487

@@ -593,6 +597,8 @@ function ManagementPage(props) {
593597
<Route exact path="/skills/:skillName" render={(props) => renderSigninIfNotSignedIn(<SkillEditPage account={account} {...props} />)} />
594598
<Route exact path="/tools" render={(props) => renderSigninIfNotSignedIn(<ToolListPage account={account} {...props} />)} />
595599
<Route exact path="/tools/:toolName" render={(props) => renderSigninIfNotSignedIn(<ToolEditPage account={account} {...props} />)} />
600+
<Route exact path="/tool-policies" render={(props) => renderSigninIfNotSignedIn(<ToolPolicyListPage account={account} {...props} />)} />
601+
<Route exact path="/tool-policies/:toolPolicyName" render={(props) => renderSigninIfNotSignedIn(<ToolPolicyEditPage account={account} {...props} />)} />
596602
<Route exact path="/servers" render={(props) => renderSigninIfNotSignedIn(<ServerListPage account={account} {...props} />)} />
597603
<Route exact path="/servers/:serverName" render={(props) => renderSigninIfNotSignedIn(<ServerEditPage account={account} {...props} />)} />
598604
<Route exact path="/server-store" render={(props) => renderSigninIfNotSignedIn(<ServerStorePage account={account} {...props} />)} />

0 commit comments

Comments
 (0)