Skip to content

Commit de48d32

Browse files
authored
♻️ The agent should support setting in-group permissions
♻️ The agent should support setting in-group permissions. #2566
2 parents 6c82569 + 21e47ff commit de48d32

12 files changed

Lines changed: 656 additions & 13 deletions

File tree

backend/consts/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class VectorDatabaseType(str, Enum):
7171
# Permission constants used by list endpoints (e.g., /agent/list, /mcp/list).
7272
PERMISSION_READ = "READ_ONLY"
7373
PERMISSION_EDIT = "EDIT"
74+
PERMISSION_PRIVATE = "PRIVATE"
7475

7576

7677
# Deployment Version Configuration

backend/consts/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ class AgentInfoRequest(BaseModel):
277277
enabled_tool_ids: Optional[List[int]] = None
278278
related_agent_ids: Optional[List[int]] = None
279279
group_ids: Optional[List[int]] = None
280+
ingroup_permission: Optional[str] = None
280281
version_no: int = 0
281282

282283

backend/database/db_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ class AgentInfo(TableBase):
226226
group_ids = Column(String, doc="Agent group IDs list")
227227
is_new = Column(Boolean, default=False, doc="Whether this agent is marked as new for the user")
228228
current_version_no = Column(Integer, nullable=True, doc="Current published version number. NULL means no version published yet")
229+
ingroup_permission = Column(String(30), doc="In-group permission: EDIT, READ_ONLY, PRIVATE")
229230

230231

231232
class ToolInstance(TableBase):

backend/services/agent_service.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from agents.preprocess_manager import preprocess_manager
1818
from services.agent_version_service import publish_version_impl
1919
from consts.const import MEMORY_SEARCH_START_MSG, MEMORY_SEARCH_DONE_MSG, MEMORY_SEARCH_FAIL_MSG, TOOL_TYPE_MAPPING, \
20-
LANGUAGE, MESSAGE_ROLE, MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ
20+
LANGUAGE, MESSAGE_ROLE, MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ, PERMISSION_PRIVATE
2121
from consts.exceptions import MemoryPreparationException
2222
from consts.model import (
2323
AgentInfoRequest,
@@ -823,7 +823,8 @@ async def update_agent_info_impl(request: AgentInfoRequest, authorization: str =
823823
"constraint_prompt": request.constraint_prompt,
824824
"few_shots_prompt": request.few_shots_prompt,
825825
"enabled": request.enabled if request.enabled is not None else True,
826-
"group_ids": convert_list_to_string(request.group_ids) if request.group_ids else user_group_ids
826+
"group_ids": convert_list_to_string(request.group_ids) if request.group_ids else user_group_ids,
827+
"ingroup_permission": request.ingroup_permission
827828
}, tenant_id=tenant_id, user_id=user_id)
828829
agent_id = created["agent_id"]
829830
else:
@@ -1325,7 +1326,10 @@ async def list_all_agent_info_impl(tenant_id: str, user_id: str) -> list[dict]:
13251326
# Apply visibility filter for DEV/USER based on group overlap
13261327
if not can_edit_all:
13271328
agent_group_ids = set(convert_string_to_list(agent.get("group_ids")))
1328-
if len(user_group_ids.intersection(agent_group_ids)) == 0 and user_id != agent.get("created_by"):
1329+
ingroup_permission = agent.get("ingroup_permission")
1330+
is_creator = str(agent.get("created_by")) == str(user_id)
1331+
# Hide agent if: no group overlap OR (ingroup_permission is PRIVATE AND user is not creator)
1332+
if len(user_group_ids.intersection(agent_group_ids)) == 0 or (ingroup_permission == PERMISSION_PRIVATE and not is_creator):
13291333
continue
13301334

13311335
# Use shared availability check function
@@ -1358,7 +1362,14 @@ async def list_all_agent_info_impl(tenant_id: str, user_id: str) -> list[dict]:
13581362
model_cache[model_id] = get_model_by_model_id(model_id, tenant_id)
13591363
model_info = model_cache.get(model_id)
13601364

1361-
permission = PERMISSION_EDIT if can_edit_all or str(agent.get("created_by")) == str(user_id) else PERMISSION_READ
1365+
# Permission logic:
1366+
# - If creator or can_edit_all: PERMISSION_EDIT
1367+
# - Otherwise: use ingroup_permission, default to PERMISSION_READ if None
1368+
if can_edit_all or str(agent.get("created_by")) == str(user_id):
1369+
permission = PERMISSION_EDIT
1370+
else:
1371+
ingroup_permission = agent.get("ingroup_permission")
1372+
permission = ingroup_permission if ingroup_permission is not None else PERMISSION_READ
13621373

13631374
simple_agent_list.append({
13641375
"agent_id": agent["agent_id"],

docker/init.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_t (
318318
provide_run_summary BOOLEAN DEFAULT FALSE,
319319
version_no INTEGER DEFAULT 0 NOT NULL,
320320
current_version_no INTEGER NULL,
321+
ingroup_permission VARCHAR(30),
321322
create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
322323
update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
323324
created_by VARCHAR(100),
@@ -371,6 +372,7 @@ COMMENT ON COLUMN nexent.ag_tenant_agent_t.delete_flag IS 'Whether it is deleted
371372
COMMENT ON COLUMN nexent.ag_tenant_agent_t.is_new IS 'Whether this agent is marked as new for the user';
372373
COMMENT ON COLUMN nexent.ag_tenant_agent_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot';
373374
COMMENT ON COLUMN nexent.ag_tenant_agent_t.current_version_no IS 'Current published version number. NULL means no version published yet';
375+
COMMENT ON COLUMN nexent.ag_tenant_agent_t.ingroup_permission IS 'In-group permission: EDIT, READ_ONLY, PRIVATE';
374376

375377
-- Create index for is_new queries
376378
CREATE INDEX IF NOT EXISTS idx_ag_tenant_agent_t_is_new
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Migration: Add ingroup_permission column to ag_tenant_agent_t table
2+
-- Date: 2025-03-02
3+
-- Description: Add ingroup_permission field to support in-group permission control for agents
4+
5+
-- Add ingroup_permission column to ag_tenant_agent_t table
6+
ALTER TABLE nexent.ag_tenant_agent_t
7+
ADD COLUMN IF NOT EXISTS ingroup_permission VARCHAR(30) DEFAULT NULL;
8+
9+
-- Add comment to the column
10+
COMMENT ON COLUMN nexent.ag_tenant_agent_t.ingroup_permission IS 'In-group permission: EDIT, READ_ONLY, PRIVATE';

frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export default function AgentGenerateDetail({
213213
mainAgentMaxStep: editedAgent.max_step || 5,
214214
agentDescription: editedAgent.description || "",
215215
group_ids: normalizeNumberArray(editedAgent.group_ids || []),
216+
ingroup_permission: editedAgent.ingroup_permission || "READ_ONLY",
216217
dutyPrompt: editedAgent.duty_prompt || "",
217218
constraintPrompt: editedAgent.constraint_prompt || "",
218219
fewShotsPrompt: editedAgent.few_shots_prompt || "",
@@ -250,7 +251,7 @@ export default function AgentGenerateDetail({
250251
});
251252
}
252253

253-
}, [currentAgentId, defaultLlmModel?.id, isCreatingMode]);
254+
}, [currentAgentId, defaultLlmModel?.id, isCreatingMode, editedAgent.ingroup_permission]);
254255

255256
// Default to selecting all groups when creating a new agent.
256257
// Only applies when groups are loaded and no group is selected yet.
@@ -537,6 +538,7 @@ export default function AgentGenerateDetail({
537538
duty_prompt: formValues.dutyPrompt,
538539
constraint_prompt: formValues.constraintPrompt,
539540
few_shots_prompt: formValues.fewShotsPrompt,
541+
ingroup_permission: formValues.ingroup_permission || "READ_ONLY",
540542
};
541543

542544
// Update profile info in global agent config store
@@ -649,6 +651,26 @@ export default function AgentGenerateDetail({
649651
</Form.Item>
650652
</Can>
651653

654+
<Can permission="group:read">
655+
<Form.Item
656+
name="ingroup_permission"
657+
label={t("tenantResources.knowledgeBase.permission")}
658+
className="mb-3"
659+
>
660+
<Select
661+
placeholder={t("tenantResources.knowledgeBase.permission")}
662+
options={[
663+
{ value: "EDIT", label: t("tenantResources.knowledgeBase.permission.EDIT") },
664+
{ value: "READ_ONLY", label: t("tenantResources.knowledgeBase.permission.READ_ONLY") },
665+
{ value: "PRIVATE", label: t("tenantResources.knowledgeBase.permission.PRIVATE") },
666+
]}
667+
onChange={(value) => {
668+
updateProfileInfo({ ingroup_permission: value });
669+
}}
670+
/>
671+
</Form.Item>
672+
</Can>
673+
652674
<Form.Item
653675
name="agentAuthor"
654676
label={t("agent.author")}

frontend/hooks/agent/useSaveGuard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const useSaveGuard = () => {
6868
business_logic_model_id: currentEditedAgent.business_logic_model_id ?? undefined,
6969
enabled_tool_ids: enabledToolIds,
7070
related_agent_ids: relatedAgentIds,
71+
ingroup_permission: currentEditedAgent.ingroup_permission ?? "READ_ONLY",
7172
});
7273

7374
if (result.success) {

frontend/services/agentConfigService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ export interface UpdateAgentInfoPayload {
391391
business_logic_model_id?: number;
392392
enabled_tool_ids?: number[];
393393
related_agent_ids?: number[];
394+
ingroup_permission?: string;
394395
}
395396

396397
export const updateAgentInfo = async (payload: UpdateAgentInfoPayload) => {
@@ -649,7 +650,7 @@ export const regenerateAgentNameBatch = async (payload: {
649650
*/
650651
export const searchAgentInfo = async (agentId: number, tenantId?: string, versionNo?: number) => {
651652
try {
652-
const url = tenantId
653+
const url = tenantId
653654
? `${API_ENDPOINTS.agent.searchInfo}?tenant_id=${encodeURIComponent(tenantId)}`
654655
: API_ENDPOINTS.agent.searchInfo;
655656
const response = await fetch(url, {
@@ -689,6 +690,7 @@ export const searchAgentInfo = async (agentId: number, tenantId?: string, versio
689690
unavailable_reasons: data.unavailable_reasons || [],
690691
sub_agent_id_list: data.sub_agent_id_list || [], // Add sub_agent_id_list
691692
group_ids: data.group_ids || [],
693+
ingroup_permission: data.ingroup_permission || "READ_ONLY",
692694
tools: data.tools
693695
? data.tools.map((tool: any) => {
694696
const params =

frontend/stores/agentConfigStore.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type EditableAgent = Pick<
3737
| "business_logic_model_id"
3838
| "sub_agent_id_list"
3939
| "group_ids"
40+
| "ingroup_permission"
4041
>;
4142

4243
interface AgentConfigStoreState {
@@ -129,6 +130,7 @@ const emptyEditableAgent: EditableAgent = {
129130
business_logic_model_id: 0,
130131
sub_agent_id_list: [],
131132
group_ids: [],
133+
ingroup_permission: "READ_ONLY",
132134
};
133135

134136
const toEditable = (agent: Agent | null): EditableAgent =>
@@ -151,6 +153,7 @@ const toEditable = (agent: Agent | null): EditableAgent =>
151153
business_logic_model_id: agent.business_logic_model_id || 0,
152154
sub_agent_id_list: agent.sub_agent_id_list || [],
153155
group_ids: agent.group_ids || [],
156+
ingroup_permission: agent.ingroup_permission || "READ_ONLY",
154157
}
155158
: { ...emptyEditableAgent };
156159

@@ -189,7 +192,8 @@ const isProfileInfoDirty = (baselineAgent: EditableAgent | null, editedAgent: Ed
189192
editedAgent.duty_prompt !== "" ||
190193
editedAgent.constraint_prompt !== "" ||
191194
editedAgent.few_shots_prompt !== "" ||
192-
normalizeArray(editedAgent.group_ids || []).length > 0
195+
normalizeArray(editedAgent.group_ids || []).length > 0 ||
196+
editedAgent.ingroup_permission !== "READ_ONLY"
193197
);
194198
}
195199
return (
@@ -205,7 +209,8 @@ const isProfileInfoDirty = (baselineAgent: EditableAgent | null, editedAgent: Ed
205209
baselineAgent.constraint_prompt !== editedAgent.constraint_prompt ||
206210
baselineAgent.few_shots_prompt !== editedAgent.few_shots_prompt ||
207211
JSON.stringify(normalizeArray(baselineAgent.group_ids ?? [])) !==
208-
JSON.stringify(normalizeArray(editedAgent.group_ids ?? []))
212+
JSON.stringify(normalizeArray(editedAgent.group_ids ?? [])) ||
213+
baselineAgent.ingroup_permission !== editedAgent.ingroup_permission
209214
);
210215
};
211216

0 commit comments

Comments
 (0)