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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/consts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class VectorDatabaseType(str, Enum):
# Permission constants used by list endpoints (e.g., /agent/list, /mcp/list).
PERMISSION_READ = "READ_ONLY"
PERMISSION_EDIT = "EDIT"
PERMISSION_PRIVATE = "PRIVATE"


# Deployment Version Configuration
Expand Down
1 change: 1 addition & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ class AgentInfoRequest(BaseModel):
enabled_tool_ids: Optional[List[int]] = None
related_agent_ids: Optional[List[int]] = None
group_ids: Optional[List[int]] = None
ingroup_permission: Optional[str] = None
version_no: int = 0


Expand Down
1 change: 1 addition & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ class AgentInfo(TableBase):
group_ids = Column(String, doc="Agent group IDs list")
is_new = Column(Boolean, default=False, doc="Whether this agent is marked as new for the user")
current_version_no = Column(Integer, nullable=True, doc="Current published version number. NULL means no version published yet")
ingroup_permission = Column(String(30), doc="In-group permission: EDIT, READ_ONLY, PRIVATE")


class ToolInstance(TableBase):
Expand Down
19 changes: 15 additions & 4 deletions backend/services/agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from agents.preprocess_manager import preprocess_manager
from services.agent_version_service import publish_version_impl
from consts.const import MEMORY_SEARCH_START_MSG, MEMORY_SEARCH_DONE_MSG, MEMORY_SEARCH_FAIL_MSG, TOOL_TYPE_MAPPING, \
LANGUAGE, MESSAGE_ROLE, MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ
LANGUAGE, MESSAGE_ROLE, MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ, PERMISSION_PRIVATE
from consts.exceptions import MemoryPreparationException
from consts.model import (
AgentInfoRequest,
Expand Down Expand Up @@ -823,7 +823,8 @@ async def update_agent_info_impl(request: AgentInfoRequest, authorization: str =
"constraint_prompt": request.constraint_prompt,
"few_shots_prompt": request.few_shots_prompt,
"enabled": request.enabled if request.enabled is not None else True,
"group_ids": convert_list_to_string(request.group_ids) if request.group_ids else user_group_ids
"group_ids": convert_list_to_string(request.group_ids) if request.group_ids else user_group_ids,
"ingroup_permission": request.ingroup_permission
}, tenant_id=tenant_id, user_id=user_id)
agent_id = created["agent_id"]
else:
Expand Down Expand Up @@ -1325,7 +1326,10 @@ async def list_all_agent_info_impl(tenant_id: str, user_id: str) -> list[dict]:
# Apply visibility filter for DEV/USER based on group overlap
if not can_edit_all:
agent_group_ids = set(convert_string_to_list(agent.get("group_ids")))
if len(user_group_ids.intersection(agent_group_ids)) == 0 and user_id != agent.get("created_by"):
ingroup_permission = agent.get("ingroup_permission")
is_creator = str(agent.get("created_by")) == str(user_id)
# Hide agent if: no group overlap OR (ingroup_permission is PRIVATE AND user is not creator)
if len(user_group_ids.intersection(agent_group_ids)) == 0 or (ingroup_permission == PERMISSION_PRIVATE and not is_creator):
continue

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

permission = PERMISSION_EDIT if can_edit_all or str(agent.get("created_by")) == str(user_id) else PERMISSION_READ
# Permission logic:
# - If creator or can_edit_all: PERMISSION_EDIT
# - Otherwise: use ingroup_permission, default to PERMISSION_READ if None
if can_edit_all or str(agent.get("created_by")) == str(user_id):
permission = PERMISSION_EDIT
else:
ingroup_permission = agent.get("ingroup_permission")
permission = ingroup_permission if ingroup_permission is not None else PERMISSION_READ

simple_agent_list.append({
"agent_id": agent["agent_id"],
Expand Down
2 changes: 2 additions & 0 deletions docker/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_t (
provide_run_summary BOOLEAN DEFAULT FALSE,
version_no INTEGER DEFAULT 0 NOT NULL,
current_version_no INTEGER NULL,
ingroup_permission VARCHAR(30),
create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
Expand Down Expand Up @@ -371,6 +372,7 @@ COMMENT ON COLUMN nexent.ag_tenant_agent_t.delete_flag IS 'Whether it is deleted
COMMENT ON COLUMN nexent.ag_tenant_agent_t.is_new IS 'Whether this agent is marked as new for the user';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.current_version_no IS 'Current published version number. NULL means no version published yet';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.ingroup_permission IS 'In-group permission: EDIT, READ_ONLY, PRIVATE';

-- Create index for is_new queries
CREATE INDEX IF NOT EXISTS idx_ag_tenant_agent_t_is_new
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Migration: Add ingroup_permission column to ag_tenant_agent_t table
-- Date: 2025-03-02
-- Description: Add ingroup_permission field to support in-group permission control for agents

-- Add ingroup_permission column to ag_tenant_agent_t table
ALTER TABLE nexent.ag_tenant_agent_t
ADD COLUMN IF NOT EXISTS ingroup_permission VARCHAR(30) DEFAULT NULL;

-- Add comment to the column
COMMENT ON COLUMN nexent.ag_tenant_agent_t.ingroup_permission IS 'In-group permission: EDIT, READ_ONLY, PRIVATE';
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export default function AgentGenerateDetail({
mainAgentMaxStep: editedAgent.max_step || 5,
agentDescription: editedAgent.description || "",
group_ids: normalizeNumberArray(editedAgent.group_ids || []),
ingroup_permission: editedAgent.ingroup_permission || "READ_ONLY",
dutyPrompt: editedAgent.duty_prompt || "",
constraintPrompt: editedAgent.constraint_prompt || "",
fewShotsPrompt: editedAgent.few_shots_prompt || "",
Expand Down Expand Up @@ -250,7 +251,7 @@ export default function AgentGenerateDetail({
});
}

}, [currentAgentId, defaultLlmModel?.id, isCreatingMode]);
}, [currentAgentId, defaultLlmModel?.id, isCreatingMode, editedAgent.ingroup_permission]);

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

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

<Can permission="group:read">
<Form.Item
name="ingroup_permission"
label={t("tenantResources.knowledgeBase.permission")}
className="mb-3"
>
<Select
placeholder={t("tenantResources.knowledgeBase.permission")}
options={[
{ value: "EDIT", label: t("tenantResources.knowledgeBase.permission.EDIT") },
{ value: "READ_ONLY", label: t("tenantResources.knowledgeBase.permission.READ_ONLY") },
{ value: "PRIVATE", label: t("tenantResources.knowledgeBase.permission.PRIVATE") },
]}
onChange={(value) => {
updateProfileInfo({ ingroup_permission: value });
}}
/>
</Form.Item>
</Can>

<Form.Item
name="agentAuthor"
label={t("agent.author")}
Expand Down
1 change: 1 addition & 0 deletions frontend/hooks/agent/useSaveGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const useSaveGuard = () => {
business_logic_model_id: currentEditedAgent.business_logic_model_id ?? undefined,
enabled_tool_ids: enabledToolIds,
related_agent_ids: relatedAgentIds,
ingroup_permission: currentEditedAgent.ingroup_permission ?? "READ_ONLY",
});

if (result.success) {
Expand Down
4 changes: 3 additions & 1 deletion frontend/services/agentConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export interface UpdateAgentInfoPayload {
business_logic_model_id?: number;
enabled_tool_ids?: number[];
related_agent_ids?: number[];
ingroup_permission?: string;
}

export const updateAgentInfo = async (payload: UpdateAgentInfoPayload) => {
Expand Down Expand Up @@ -649,7 +650,7 @@ export const regenerateAgentNameBatch = async (payload: {
*/
export const searchAgentInfo = async (agentId: number, tenantId?: string, versionNo?: number) => {
try {
const url = tenantId
const url = tenantId
? `${API_ENDPOINTS.agent.searchInfo}?tenant_id=${encodeURIComponent(tenantId)}`
: API_ENDPOINTS.agent.searchInfo;
const response = await fetch(url, {
Expand Down Expand Up @@ -689,6 +690,7 @@ export const searchAgentInfo = async (agentId: number, tenantId?: string, versio
unavailable_reasons: data.unavailable_reasons || [],
sub_agent_id_list: data.sub_agent_id_list || [], // Add sub_agent_id_list
group_ids: data.group_ids || [],
ingroup_permission: data.ingroup_permission || "READ_ONLY",
tools: data.tools
? data.tools.map((tool: any) => {
const params =
Expand Down
9 changes: 7 additions & 2 deletions frontend/stores/agentConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type EditableAgent = Pick<
| "business_logic_model_id"
| "sub_agent_id_list"
| "group_ids"
| "ingroup_permission"
>;

interface AgentConfigStoreState {
Expand Down Expand Up @@ -129,6 +130,7 @@ const emptyEditableAgent: EditableAgent = {
business_logic_model_id: 0,
sub_agent_id_list: [],
group_ids: [],
ingroup_permission: "READ_ONLY",
};

const toEditable = (agent: Agent | null): EditableAgent =>
Expand All @@ -151,6 +153,7 @@ const toEditable = (agent: Agent | null): EditableAgent =>
business_logic_model_id: agent.business_logic_model_id || 0,
sub_agent_id_list: agent.sub_agent_id_list || [],
group_ids: agent.group_ids || [],
ingroup_permission: agent.ingroup_permission || "READ_ONLY",
}
: { ...emptyEditableAgent };

Expand Down Expand Up @@ -189,7 +192,8 @@ const isProfileInfoDirty = (baselineAgent: EditableAgent | null, editedAgent: Ed
editedAgent.duty_prompt !== "" ||
editedAgent.constraint_prompt !== "" ||
editedAgent.few_shots_prompt !== "" ||
normalizeArray(editedAgent.group_ids || []).length > 0
normalizeArray(editedAgent.group_ids || []).length > 0 ||
editedAgent.ingroup_permission !== "READ_ONLY"
);
}
return (
Expand All @@ -205,7 +209,8 @@ const isProfileInfoDirty = (baselineAgent: EditableAgent | null, editedAgent: Ed
baselineAgent.constraint_prompt !== editedAgent.constraint_prompt ||
baselineAgent.few_shots_prompt !== editedAgent.few_shots_prompt ||
JSON.stringify(normalizeArray(baselineAgent.group_ids ?? [])) !==
JSON.stringify(normalizeArray(editedAgent.group_ids ?? []))
JSON.stringify(normalizeArray(editedAgent.group_ids ?? [])) ||
baselineAgent.ingroup_permission !== editedAgent.ingroup_permission
);
};

Expand Down
2 changes: 2 additions & 0 deletions frontend/types/agentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type AgentProfileInfo = Partial<
| "constraint_prompt"
| "few_shots_prompt"
| "group_ids"
| "ingroup_permission"
>
>;

Expand Down Expand Up @@ -51,6 +52,7 @@ export interface Agent {
is_new?: boolean;
sub_agent_id_list?: number[];
group_ids?: number[];
ingroup_permission?: "EDIT" | "READ_ONLY" | "PRIVATE";
/**
* Per-agent permission returned by /agent/list.
* EDIT: editable, READ_ONLY: read-only.
Expand Down
Loading
Loading