Add HR agent onboarding model and actor enforcement

This commit is contained in:
Abhimanyu Saharan
2026-02-02 01:36:32 +05:30
parent d18a38e666
commit 886db3fa97
15 changed files with 781 additions and 38 deletions

View File

@@ -3,10 +3,10 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select from sqlmodel import Session, select
from app.api.utils import log_activity from app.api.utils import log_activity, get_actor_employee_id
from app.db.session import get_session from app.db.session import get_session
from app.models.hr import EmploymentAction, HeadcountRequest from app.models.hr import EmploymentAction, HeadcountRequest, AgentOnboarding
from app.schemas.hr import EmploymentActionCreate, HeadcountRequestCreate, HeadcountRequestUpdate from app.schemas.hr import EmploymentActionCreate, HeadcountRequestCreate, HeadcountRequestUpdate, AgentOnboardingCreate, AgentOnboardingUpdate
router = APIRouter(prefix="/hr", tags=["hr"]) router = APIRouter(prefix="/hr", tags=["hr"])
@@ -17,18 +17,18 @@ def list_headcount_requests(session: Session = Depends(get_session)):
@router.post("/headcount", response_model=HeadcountRequest) @router.post("/headcount", response_model=HeadcountRequest)
def create_headcount_request(payload: HeadcountRequestCreate, session: Session = Depends(get_session)): def create_headcount_request(payload: HeadcountRequestCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
req = HeadcountRequest(**payload.model_dump()) req = HeadcountRequest(**payload.model_dump())
session.add(req) session.add(req)
session.commit() session.commit()
session.refresh(req) session.refresh(req)
log_activity(session, actor_employee_id=req.requested_by_manager_id, entity_type="headcount_request", entity_id=req.id, verb="submitted") log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="submitted")
session.commit() session.commit()
return req return req
@router.patch("/headcount/{request_id}", response_model=HeadcountRequest) @router.patch("/headcount/{request_id}", response_model=HeadcountRequest)
def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, session: Session = Depends(get_session)): def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
req = session.get(HeadcountRequest, request_id) req = session.get(HeadcountRequest, request_id)
if not req: if not req:
raise HTTPException(status_code=404, detail="Request not found") raise HTTPException(status_code=404, detail="Request not found")
@@ -40,7 +40,7 @@ def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, s
session.add(req) session.add(req)
session.commit() session.commit()
session.refresh(req) session.refresh(req)
log_activity(session, actor_employee_id=req.requested_by_manager_id, entity_type="headcount_request", entity_id=req.id, verb="updated", payload=data) log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="updated", payload=data)
session.commit() session.commit()
return req return req
@@ -51,11 +51,47 @@ def list_employment_actions(session: Session = Depends(get_session)):
@router.post("/actions", response_model=EmploymentAction) @router.post("/actions", response_model=EmploymentAction)
def create_employment_action(payload: EmploymentActionCreate, session: Session = Depends(get_session)): def create_employment_action(payload: EmploymentActionCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
action = EmploymentAction(**payload.model_dump()) action = EmploymentAction(**payload.model_dump())
session.add(action) session.add(action)
session.commit() session.commit()
session.refresh(action) session.refresh(action)
log_activity(session, actor_employee_id=action.issued_by_employee_id, entity_type="employment_action", entity_id=action.id, verb=action.action_type, payload={"employee_id": action.employee_id}) log_activity(session, actor_employee_id=actor_employee_id, entity_type="employment_action", entity_id=action.id, verb=action.action_type, payload={"employee_id": action.employee_id})
session.commit() session.commit()
return action return action
@router.get("/onboarding", response_model=list[AgentOnboarding])
def list_agent_onboarding(session: Session = Depends(get_session)):
return session.exec(select(AgentOnboarding).order_by(AgentOnboarding.id.desc())).all()
@router.post("/onboarding", response_model=AgentOnboarding)
def create_agent_onboarding(payload: AgentOnboardingCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
item = AgentOnboarding(**payload.model_dump())
session.add(item)
session.commit()
session.refresh(item)
log_activity(session, actor_employee_id=actor_employee_id, entity_type="agent_onboarding", entity_id=item.id, verb="created", payload={"agent_name": item.agent_name, "status": item.status})
session.commit()
return item
@router.patch("/onboarding/{onboarding_id}", response_model=AgentOnboarding)
def update_agent_onboarding(onboarding_id: int, payload: AgentOnboardingUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
item = session.get(AgentOnboarding, onboarding_id)
if not item:
raise HTTPException(status_code=404, detail="Onboarding record not found")
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(item, k, v)
from datetime import datetime
item.updated_at = datetime.utcnow()
session.add(item)
session.commit()
session.refresh(item)
log_activity(session, actor_employee_id=actor_employee_id, entity_type="agent_onboarding", entity_id=item.id, verb="updated", payload=data)
session.commit()
return item

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select from sqlmodel import Session, select
from app.api.utils import log_activity from app.api.utils import log_activity, get_actor_employee_id
from app.db.session import get_session from app.db.session import get_session
from app.models.org import Department, Employee from app.models.org import Department, Employee
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate
@@ -17,18 +17,18 @@ def list_departments(session: Session = Depends(get_session)):
@router.post("/departments", response_model=Department) @router.post("/departments", response_model=Department)
def create_department(payload: DepartmentCreate, session: Session = Depends(get_session)): def create_department(payload: DepartmentCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
dept = Department(name=payload.name, head_employee_id=payload.head_employee_id) dept = Department(name=payload.name, head_employee_id=payload.head_employee_id)
session.add(dept) session.add(dept)
session.commit() session.commit()
session.refresh(dept) session.refresh(dept)
log_activity(session, actor_employee_id=None, entity_type="department", entity_id=dept.id, verb="created", payload={"name": dept.name}) log_activity(session, actor_employee_id=actor_employee_id, entity_type="department", entity_id=dept.id, verb="created", payload={"name": dept.name})
session.commit() session.commit()
return dept return dept
@router.patch("/departments/{department_id}", response_model=Department) @router.patch("/departments/{department_id}", response_model=Department)
def update_department(department_id: int, payload: DepartmentUpdate, session: Session = Depends(get_session)): def update_department(department_id: int, payload: DepartmentUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
dept = session.get(Department, department_id) dept = session.get(Department, department_id)
if not dept: if not dept:
raise HTTPException(status_code=404, detail="Department not found") raise HTTPException(status_code=404, detail="Department not found")
@@ -40,7 +40,7 @@ def update_department(department_id: int, payload: DepartmentUpdate, session: Se
session.add(dept) session.add(dept)
session.commit() session.commit()
session.refresh(dept) session.refresh(dept)
log_activity(session, actor_employee_id=None, entity_type="department", entity_id=dept.id, verb="updated", payload=data) log_activity(session, actor_employee_id=actor_employee_id, entity_type="department", entity_id=dept.id, verb="updated", payload=data)
session.commit() session.commit()
return dept return dept
@@ -51,18 +51,18 @@ def list_employees(session: Session = Depends(get_session)):
@router.post("/employees", response_model=Employee) @router.post("/employees", response_model=Employee)
def create_employee(payload: EmployeeCreate, session: Session = Depends(get_session)): def create_employee(payload: EmployeeCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
emp = Employee(**payload.model_dump()) emp = Employee(**payload.model_dump())
session.add(emp) session.add(emp)
session.commit() session.commit()
session.refresh(emp) session.refresh(emp)
log_activity(session, actor_employee_id=None, entity_type="employee", entity_id=emp.id, verb="created", payload={"name": emp.name, "type": emp.employee_type}) log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="created", payload={"name": emp.name, "type": emp.employee_type})
session.commit() session.commit()
return emp return emp
@router.patch("/employees/{employee_id}", response_model=Employee) @router.patch("/employees/{employee_id}", response_model=Employee)
def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session = Depends(get_session)): def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
emp = session.get(Employee, employee_id) emp = session.get(Employee, employee_id)
if not emp: if not emp:
raise HTTPException(status_code=404, detail="Employee not found") raise HTTPException(status_code=404, detail="Employee not found")
@@ -74,6 +74,6 @@ def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session
session.add(emp) session.add(emp)
session.commit() session.commit()
session.refresh(emp) session.refresh(emp)
log_activity(session, actor_employee_id=None, entity_type="employee", entity_id=emp.id, verb="updated", payload=data) log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="updated", payload=data)
session.commit() session.commit()
return emp return emp

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select from sqlmodel import Session, select
from app.api.utils import log_activity from app.api.utils import log_activity, get_actor_employee_id
from app.db.session import get_session from app.db.session import get_session
from app.models.projects import Project, ProjectMember from app.models.projects import Project, ProjectMember
from app.schemas.projects import ProjectCreate, ProjectUpdate from app.schemas.projects import ProjectCreate, ProjectUpdate
@@ -17,18 +17,18 @@ def list_projects(session: Session = Depends(get_session)):
@router.post("", response_model=Project) @router.post("", response_model=Project)
def create_project(payload: ProjectCreate, session: Session = Depends(get_session)): def create_project(payload: ProjectCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
proj = Project(**payload.model_dump()) proj = Project(**payload.model_dump())
session.add(proj) session.add(proj)
session.commit() session.commit()
session.refresh(proj) session.refresh(proj)
log_activity(session, actor_employee_id=None, entity_type="project", entity_id=proj.id, verb="created", payload={"name": proj.name}) log_activity(session, actor_employee_id=actor_employee_id, entity_type="project", entity_id=proj.id, verb="created", payload={"name": proj.name})
session.commit() session.commit()
return proj return proj
@router.patch("/{project_id}", response_model=Project) @router.patch("/{project_id}", response_model=Project)
def update_project(project_id: int, payload: ProjectUpdate, session: Session = Depends(get_session)): def update_project(project_id: int, payload: ProjectUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
proj = session.get(Project, project_id) proj = session.get(Project, project_id)
if not proj: if not proj:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
@@ -40,7 +40,7 @@ def update_project(project_id: int, payload: ProjectUpdate, session: Session = D
session.add(proj) session.add(proj)
session.commit() session.commit()
session.refresh(proj) session.refresh(proj)
log_activity(session, actor_employee_id=None, entity_type="project", entity_id=proj.id, verb="updated", payload=data) log_activity(session, actor_employee_id=actor_employee_id, entity_type="project", entity_id=proj.id, verb="updated", payload=data)
session.commit() session.commit()
return proj return proj
@@ -53,7 +53,7 @@ def list_project_members(project_id: int, session: Session = Depends(get_session
@router.post("/{project_id}/members", response_model=ProjectMember) @router.post("/{project_id}/members", response_model=ProjectMember)
def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session)): def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
existing = session.exec(select(ProjectMember).where(ProjectMember.project_id == project_id, ProjectMember.employee_id == payload.employee_id)).first() existing = session.exec(select(ProjectMember).where(ProjectMember.project_id == project_id, ProjectMember.employee_id == payload.employee_id)).first()
if existing: if existing:
raise HTTPException(status_code=409, detail="Member already added") raise HTTPException(status_code=409, detail="Member already added")
@@ -63,7 +63,7 @@ def add_project_member(project_id: int, payload: ProjectMember, session: Session
session.refresh(member) session.refresh(member)
log_activity( log_activity(
session, session,
actor_employee_id=None, actor_employee_id=actor_employee_id,
entity_type="project_member", entity_type="project_member",
entity_id=member.id, entity_id=member.id,
verb="added", verb="added",
@@ -74,7 +74,7 @@ def add_project_member(project_id: int, payload: ProjectMember, session: Session
@router.delete("/{project_id}/members/{member_id}") @router.delete("/{project_id}/members/{member_id}")
def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session)): def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
member = session.get(ProjectMember, member_id) member = session.get(ProjectMember, member_id)
if not member or member.project_id != project_id: if not member or member.project_id != project_id:
raise HTTPException(status_code=404, detail="Project member not found") raise HTTPException(status_code=404, detail="Project member not found")
@@ -82,7 +82,7 @@ def remove_project_member(project_id: int, member_id: int, session: Session = De
session.commit() session.commit()
log_activity( log_activity(
session, session,
actor_employee_id=None, actor_employee_id=actor_employee_id,
entity_type="project_member", entity_type="project_member",
entity_id=member_id, entity_id=member_id,
verb="removed", verb="removed",
@@ -93,7 +93,7 @@ def remove_project_member(project_id: int, member_id: int, session: Session = De
@router.patch("/{project_id}/members/{member_id}", response_model=ProjectMember) @router.patch("/{project_id}/members/{member_id}", response_model=ProjectMember)
def update_project_member(project_id: int, member_id: int, payload: ProjectMember, session: Session = Depends(get_session)): def update_project_member(project_id: int, member_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
member = session.get(ProjectMember, member_id) member = session.get(ProjectMember, member_id)
if not member or member.project_id != project_id: if not member or member.project_id != project_id:
raise HTTPException(status_code=404, detail="Project member not found") raise HTTPException(status_code=404, detail="Project member not found")
@@ -106,7 +106,7 @@ def update_project_member(project_id: int, member_id: int, payload: ProjectMembe
session.refresh(member) session.refresh(member)
log_activity( log_activity(
session, session,
actor_employee_id=None, actor_employee_id=actor_employee_id,
entity_type="project_member", entity_type="project_member",
entity_id=member.id, entity_id=member.id,
verb="updated", verb="updated",

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import json import json
from typing import Any from typing import Any
from fastapi import Header, HTTPException
from sqlmodel import Session from sqlmodel import Session
from app.models.activity import Activity from app.models.activity import Activity
@@ -26,3 +27,11 @@ def log_activity(
payload_json=json.dumps(payload) if payload is not None else None, payload_json=json.dumps(payload) if payload is not None else None,
) )
) )
def get_actor_employee_id(
x_actor_employee_id: int | None = Header(default=None, alias="X-Actor-Employee-Id"),
) -> int:
if x_actor_employee_id is None:
raise HTTPException(status_code=400, detail="X-Actor-Employee-Id required")
return x_actor_employee_id

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select from sqlmodel import Session, select
from app.api.utils import log_activity from app.api.utils import log_activity, get_actor_employee_id
from app.db.session import get_session from app.db.session import get_session
from app.models.work import Task, TaskComment from app.models.work import Task, TaskComment
from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate
@@ -24,7 +24,9 @@ def list_tasks(project_id: int | None = None, session: Session = Depends(get_ses
@router.post("/tasks", response_model=Task) @router.post("/tasks", response_model=Task)
def create_task(payload: TaskCreate, session: Session = Depends(get_session)): def create_task(payload: TaskCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
if payload.created_by_employee_id is None:
payload = TaskCreate(**{**payload.model_dump(), "created_by_employee_id": actor_employee_id})
task = Task(**payload.model_dump()) task = Task(**payload.model_dump())
if task.status not in ALLOWED_STATUSES: if task.status not in ALLOWED_STATUSES:
raise HTTPException(status_code=400, detail="Invalid status") raise HTTPException(status_code=400, detail="Invalid status")
@@ -34,7 +36,7 @@ def create_task(payload: TaskCreate, session: Session = Depends(get_session)):
session.refresh(task) session.refresh(task)
log_activity( log_activity(
session, session,
actor_employee_id=task.created_by_employee_id, actor_employee_id=actor_employee_id,
entity_type="task", entity_type="task",
entity_id=task.id, entity_id=task.id,
verb="created", verb="created",
@@ -45,7 +47,7 @@ def create_task(payload: TaskCreate, session: Session = Depends(get_session)):
@router.patch("/tasks/{task_id}", response_model=Task) @router.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(get_session)): def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
task = session.get(Task, task_id) task = session.get(Task, task_id)
if not task: if not task:
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
@@ -60,19 +62,19 @@ def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(ge
session.add(task) session.add(task)
session.commit() session.commit()
session.refresh(task) session.refresh(task)
log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task.id, verb="updated", payload=data) log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task.id, verb="updated", payload=data)
session.commit() session.commit()
return task return task
@router.delete("/tasks/{task_id}") @router.delete("/tasks/{task_id}")
def delete_task(task_id: int, session: Session = Depends(get_session)): def delete_task(task_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
task = session.get(Task, task_id) task = session.get(Task, task_id)
if not task: if not task:
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
session.delete(task) session.delete(task)
session.commit() session.commit()
log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task_id, verb="deleted") log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task_id, verb="deleted")
session.commit() session.commit()
return {"ok": True} return {"ok": True}
@@ -83,11 +85,13 @@ def list_task_comments(task_id: int, session: Session = Depends(get_session)):
@router.post("/task-comments", response_model=TaskComment) @router.post("/task-comments", response_model=TaskComment)
def create_task_comment(payload: TaskCommentCreate, session: Session = Depends(get_session)): def create_task_comment(payload: TaskCommentCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
if payload.author_employee_id is None:
payload = TaskCommentCreate(**{**payload.model_dump(), "author_employee_id": actor_employee_id})
c = TaskComment(**payload.model_dump()) c = TaskComment(**payload.model_dump())
session.add(c) session.add(c)
session.commit() session.commit()
session.refresh(c) session.refresh(c)
log_activity(session, actor_employee_id=c.author_employee_id, entity_type="task", entity_id=c.task_id, verb="commented") log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=c.task_id, verb="commented")
session.commit() session.commit()
return c return c

View File

@@ -33,3 +33,23 @@ class EmploymentAction(SQLModel, table=True):
notes: str | None = None notes: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=datetime.utcnow)
class AgentOnboarding(SQLModel, table=True):
__tablename__ = "agent_onboardings"
id: int | None = Field(default=None, primary_key=True)
agent_name: str
role_title: str
prompt: str
cron_interval_ms: int | None = None
tools_json: str | None = None
owner_hr_id: int | None = Field(default=None, foreign_key="employees.id")
status: str = Field(default="planned") # planned|spawning|spawned|verified|blocked
spawned_agent_id: str | None = None
session_key: str | None = None
notes: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -22,3 +22,30 @@ class EmploymentActionCreate(SQLModel):
issued_by_employee_id: int issued_by_employee_id: int
action_type: str action_type: str
notes: str | None = None notes: str | None = None
class AgentOnboardingCreate(SQLModel):
agent_name: str
role_title: str
prompt: str
cron_interval_ms: int | None = None
tools_json: str | None = None
owner_hr_id: int | None = None
status: str = "planned"
spawned_agent_id: str | None = None
session_key: str | None = None
notes: str | None = None
class AgentOnboardingUpdate(SQLModel):
agent_name: str | None = None
role_title: str | None = None
prompt: str | None = None
cron_interval_ms: int | None = None
tools_json: str | None = None
owner_hr_id: int | None = None
status: str | None = None
spawned_agent_id: str | None = None
session_key: str | None = None
notes: str | None = None

View File

@@ -21,6 +21,9 @@ import type {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import type { import type {
AgentOnboarding,
AgentOnboardingCreate,
AgentOnboardingUpdate,
EmploymentAction, EmploymentAction,
EmploymentActionCreate, EmploymentActionCreate,
HTTPValidationError, HTTPValidationError,
@@ -748,3 +751,434 @@ export const useCreateEmploymentActionHrActionsPost = <
queryClient, queryClient,
); );
}; };
/**
* @summary List Agent Onboarding
*/
export type listAgentOnboardingHrOnboardingGetResponse200 = {
data: AgentOnboarding[];
status: 200;
};
export type listAgentOnboardingHrOnboardingGetResponseSuccess =
listAgentOnboardingHrOnboardingGetResponse200 & {
headers: Headers;
};
export type listAgentOnboardingHrOnboardingGetResponse =
listAgentOnboardingHrOnboardingGetResponseSuccess;
export const getListAgentOnboardingHrOnboardingGetUrl = () => {
return `/hr/onboarding`;
};
export const listAgentOnboardingHrOnboardingGet = async (
options?: RequestInit,
): Promise<listAgentOnboardingHrOnboardingGetResponse> => {
return customFetch<listAgentOnboardingHrOnboardingGetResponse>(
getListAgentOnboardingHrOnboardingGetUrl(),
{
...options,
method: "GET",
},
);
};
export const getListAgentOnboardingHrOnboardingGetQueryKey = () => {
return [`/hr/onboarding`] as const;
};
export const getListAgentOnboardingHrOnboardingGetQueryOptions = <
TData = Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError = unknown,
>(options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListAgentOnboardingHrOnboardingGetQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>
> = ({ signal }) =>
listAgentOnboardingHrOnboardingGet({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListAgentOnboardingHrOnboardingGetQueryResult = NonNullable<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>
>;
export type ListAgentOnboardingHrOnboardingGetQueryError = unknown;
export function useListAgentOnboardingHrOnboardingGet<
TData = Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError = unknown,
>(
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListAgentOnboardingHrOnboardingGet<
TData = Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError = unknown,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListAgentOnboardingHrOnboardingGet<
TData = Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError = unknown,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary List Agent Onboarding
*/
export function useListAgentOnboardingHrOnboardingGet<
TData = Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError = unknown,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listAgentOnboardingHrOnboardingGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getListAgentOnboardingHrOnboardingGetQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Create Agent Onboarding
*/
export type createAgentOnboardingHrOnboardingPostResponse200 = {
data: AgentOnboarding;
status: 200;
};
export type createAgentOnboardingHrOnboardingPostResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type createAgentOnboardingHrOnboardingPostResponseSuccess =
createAgentOnboardingHrOnboardingPostResponse200 & {
headers: Headers;
};
export type createAgentOnboardingHrOnboardingPostResponseError =
createAgentOnboardingHrOnboardingPostResponse422 & {
headers: Headers;
};
export type createAgentOnboardingHrOnboardingPostResponse =
| createAgentOnboardingHrOnboardingPostResponseSuccess
| createAgentOnboardingHrOnboardingPostResponseError;
export const getCreateAgentOnboardingHrOnboardingPostUrl = () => {
return `/hr/onboarding`;
};
export const createAgentOnboardingHrOnboardingPost = async (
agentOnboardingCreate: AgentOnboardingCreate,
options?: RequestInit,
): Promise<createAgentOnboardingHrOnboardingPostResponse> => {
return customFetch<createAgentOnboardingHrOnboardingPostResponse>(
getCreateAgentOnboardingHrOnboardingPostUrl(),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(agentOnboardingCreate),
},
);
};
export const getCreateAgentOnboardingHrOnboardingPostMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>,
TError,
{ data: AgentOnboardingCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>,
TError,
{ data: AgentOnboardingCreate },
TContext
> => {
const mutationKey = ["createAgentOnboardingHrOnboardingPost"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>,
{ data: AgentOnboardingCreate }
> = (props) => {
const { data } = props ?? {};
return createAgentOnboardingHrOnboardingPost(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type CreateAgentOnboardingHrOnboardingPostMutationResult = NonNullable<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>
>;
export type CreateAgentOnboardingHrOnboardingPostMutationBody =
AgentOnboardingCreate;
export type CreateAgentOnboardingHrOnboardingPostMutationError =
HTTPValidationError;
/**
* @summary Create Agent Onboarding
*/
export const useCreateAgentOnboardingHrOnboardingPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>,
TError,
{ data: AgentOnboardingCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof createAgentOnboardingHrOnboardingPost>>,
TError,
{ data: AgentOnboardingCreate },
TContext
> => {
return useMutation(
getCreateAgentOnboardingHrOnboardingPostMutationOptions(options),
queryClient,
);
};
/**
* @summary Update Agent Onboarding
*/
export type updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse200 = {
data: AgentOnboarding;
status: 200;
};
export type updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type updateAgentOnboardingHrOnboardingOnboardingIdPatchResponseSuccess =
updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse200 & {
headers: Headers;
};
export type updateAgentOnboardingHrOnboardingOnboardingIdPatchResponseError =
updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse422 & {
headers: Headers;
};
export type updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse =
| updateAgentOnboardingHrOnboardingOnboardingIdPatchResponseSuccess
| updateAgentOnboardingHrOnboardingOnboardingIdPatchResponseError;
export const getUpdateAgentOnboardingHrOnboardingOnboardingIdPatchUrl = (
onboardingId: number,
) => {
return `/hr/onboarding/${onboardingId}`;
};
export const updateAgentOnboardingHrOnboardingOnboardingIdPatch = async (
onboardingId: number,
agentOnboardingUpdate: AgentOnboardingUpdate,
options?: RequestInit,
): Promise<updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse> => {
return customFetch<updateAgentOnboardingHrOnboardingOnboardingIdPatchResponse>(
getUpdateAgentOnboardingHrOnboardingOnboardingIdPatchUrl(onboardingId),
{
...options,
method: "PATCH",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(agentOnboardingUpdate),
},
);
};
export const getUpdateAgentOnboardingHrOnboardingOnboardingIdPatchMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>,
TError,
{ onboardingId: number; data: AgentOnboardingUpdate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>,
TError,
{ onboardingId: number; data: AgentOnboardingUpdate },
TContext
> => {
const mutationKey = ["updateAgentOnboardingHrOnboardingOnboardingIdPatch"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>,
{ onboardingId: number; data: AgentOnboardingUpdate }
> = (props) => {
const { onboardingId, data } = props ?? {};
return updateAgentOnboardingHrOnboardingOnboardingIdPatch(
onboardingId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateAgentOnboardingHrOnboardingOnboardingIdPatchMutationResult =
NonNullable<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>
>;
export type UpdateAgentOnboardingHrOnboardingOnboardingIdPatchMutationBody =
AgentOnboardingUpdate;
export type UpdateAgentOnboardingHrOnboardingOnboardingIdPatchMutationError =
HTTPValidationError;
/**
* @summary Update Agent Onboarding
*/
export const useUpdateAgentOnboardingHrOnboardingOnboardingIdPatch = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>,
TError,
{ onboardingId: number; data: AgentOnboardingUpdate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof updateAgentOnboardingHrOnboardingOnboardingIdPatch>
>,
TError,
{ onboardingId: number; data: AgentOnboardingUpdate },
TContext
> => {
return useMutation(
getUpdateAgentOnboardingHrOnboardingOnboardingIdPatchMutationOptions(
options,
),
queryClient,
);
};

View File

@@ -0,0 +1,22 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* OpenClaw Agency API
* OpenAPI spec version: 0.3.0
*/
export interface AgentOnboarding {
id?: number | null;
agent_name: string;
role_title: string;
prompt: string;
cron_interval_ms?: number | null;
tools_json?: string | null;
owner_hr_id?: number | null;
status?: string;
spawned_agent_id?: string | null;
session_key?: string | null;
notes?: string | null;
created_at?: string;
updated_at?: string;
}

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* OpenClaw Agency API
* OpenAPI spec version: 0.3.0
*/
export interface AgentOnboardingCreate {
agent_name: string;
role_title: string;
prompt: string;
cron_interval_ms?: number | null;
tools_json?: string | null;
owner_hr_id?: number | null;
status?: string;
spawned_agent_id?: string | null;
session_key?: string | null;
notes?: string | null;
}

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* OpenClaw Agency API
* OpenAPI spec version: 0.3.0
*/
export interface AgentOnboardingUpdate {
agent_name?: string | null;
role_title?: string | null;
prompt?: string | null;
cron_interval_ms?: number | null;
tools_json?: string | null;
owner_hr_id?: number | null;
status?: string | null;
spawned_agent_id?: string | null;
session_key?: string | null;
notes?: string | null;
}

View File

@@ -5,6 +5,9 @@
* OpenAPI spec version: 0.3.0 * OpenAPI spec version: 0.3.0
*/ */
export * from "./agentOnboarding";
export * from "./agentOnboardingCreate";
export * from "./agentOnboardingUpdate";
export * from "./department"; export * from "./department";
export * from "./departmentCreate"; export * from "./departmentCreate";
export * from "./departmentUpdate"; export * from "./departmentUpdate";

View File

@@ -1,3 +1,17 @@
function getActorId(): string | undefined {
if (typeof window !== "undefined") {
const stored = window.localStorage.getItem("actor_employee_id");
if (stored) return stored;
const env = process.env.NEXT_PUBLIC_ACTOR_EMPLOYEE_ID;
if (env) {
window.localStorage.setItem("actor_employee_id", env);
return env;
}
return undefined;
}
return process.env.NEXT_PUBLIC_ACTOR_EMPLOYEE_ID;
}
export async function customFetch<T>( export async function customFetch<T>(
url: string, url: string,
options: RequestInit, options: RequestInit,
@@ -9,6 +23,7 @@ export async function customFetch<T>(
...options, ...options,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(getActorId() ? { "X-Actor-Employee-Id": String(getActorId()) } : {}),
...(options.headers ?? {}), ...(options.headers ?? {}),
}, },
}); });

View File

@@ -2,6 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import styles from "./Shell.module.css"; import styles from "./Shell.module.css";
const NAV = [ const NAV = [
@@ -14,6 +15,16 @@ const NAV = [
export function Shell({ children }: { children: React.ReactNode }) { export function Shell({ children }: { children: React.ReactNode }) {
const path = usePathname(); const path = usePathname();
const [actorId, setActorId] = useState("");
useEffect(() => {
try {
const stored = window.localStorage.getItem("actor_employee_id");
if (stored) setActorId(stored);
} catch {
// ignore
}
}, []);
return ( return (
<div className={styles.shell}> <div className={styles.shell}>
<aside className={styles.sidebar}> <aside className={styles.sidebar}>
@@ -32,6 +43,25 @@ export function Shell({ children }: { children: React.ReactNode }) {
</Link> </Link>
))} ))}
</nav> </nav>
<div className={styles.mono} style={{ marginTop: 16 }}>
<div style={{ fontWeight: 600, marginBottom: 6 }}>Actor ID</div>
<input
value={actorId}
onChange={(e) => {
const v = e.target.value;
setActorId(v);
try {
if (v) window.localStorage.setItem("actor_employee_id", v);
else window.localStorage.removeItem("actor_employee_id");
} catch {
// ignore
}
}}
placeholder="e.g. 1"
style={{ width: "100%", padding: "6px 8px", borderRadius: 6, border: "1px solid #333", background: "transparent" }}
/>
</div>
<div className={styles.mono} style={{ marginTop: "auto" }}> <div className={styles.mono} style={{ marginTop: "auto" }}>
Tip: use your machine IP + ports<br /> Tip: use your machine IP + ports<br />
<span className={styles.kbd}>:3000</span> UI &nbsp; <span className={styles.kbd}>:8000</span> API <span className={styles.kbd}>:3000</span> UI &nbsp; <span className={styles.kbd}>:8000</span> API

View File

@@ -13,6 +13,9 @@ import {
useCreateEmploymentActionHrActionsPost, useCreateEmploymentActionHrActionsPost,
useListHeadcountRequestsHrHeadcountGet, useListHeadcountRequestsHrHeadcountGet,
useListEmploymentActionsHrActionsGet, useListEmploymentActionsHrActionsGet,
useListAgentOnboardingHrOnboardingGet,
useCreateAgentOnboardingHrOnboardingPost,
useUpdateAgentOnboardingHrOnboardingOnboardingIdPatch,
} from "@/api/generated/hr/hr"; } from "@/api/generated/hr/hr";
import { useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet } from "@/api/generated/org/org"; import { useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet } from "@/api/generated/org/org";
@@ -35,6 +38,14 @@ export default function HRPage() {
const [actType, setActType] = useState("praise"); const [actType, setActType] = useState("praise");
const [actNotes, setActNotes] = useState(""); const [actNotes, setActNotes] = useState("");
const [onboardAgentName, setOnboardAgentName] = useState("");
const [onboardRole, setOnboardRole] = useState("");
const [onboardPrompt, setOnboardPrompt] = useState("");
const [onboardCronMs, setOnboardCronMs] = useState("");
const [onboardTools, setOnboardTools] = useState("");
const [onboardOwnerId, setOnboardOwnerId] = useState<string>("");
const [onboardNotes, setOnboardNotes] = useState("");
const createHeadcount = useCreateHeadcountRequestHrHeadcountPost({ const createHeadcount = useCreateHeadcountRequestHrHeadcountPost({
mutation: { mutation: {
onSuccess: () => { onSuccess: () => {
@@ -200,6 +211,100 @@ export default function HRPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<div className="mt-6 grid gap-4">
<Card>
<CardHeader>
<CardTitle>Agent onboarding</CardTitle>
<CardDescription>HR logs prompts, cron, tools, and spawn status (Mission Control only).</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<div className="space-y-3">
<Input placeholder="Agent name" value={onboardAgentName} onChange={(e) => setOnboardAgentName(e.target.value)} />
<Input placeholder="Role/title" value={onboardRole} onChange={(e) => setOnboardRole(e.target.value)} />
<Textarea placeholder="Prompt / system instructions" value={onboardPrompt} onChange={(e) => setOnboardPrompt(e.target.value)} />
<Input placeholder="Cron interval ms (e.g. 300000)" value={onboardCronMs} onChange={(e) => setOnboardCronMs(e.target.value)} />
<Textarea placeholder="Tools/permissions (JSON or text)" value={onboardTools} onChange={(e) => setOnboardTools(e.target.value)} />
<Select value={onboardOwnerId} onChange={(e) => setOnboardOwnerId(e.target.value)}>
<option value="">Owner (HR)</option>
{(employees.data ?? []).map((e) => (
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
))}
</Select>
<Textarea placeholder="Notes" value={onboardNotes} onChange={(e) => setOnboardNotes(e.target.value)} />
<Button
onClick={() =>
createOnboarding.mutate({
data: {
agent_name: onboardAgentName,
role_title: onboardRole,
prompt: onboardPrompt,
cron_interval_ms: onboardCronMs ? Number(onboardCronMs) : null,
tools_json: onboardTools.trim() ? onboardTools : null,
owner_hr_id: onboardOwnerId ? Number(onboardOwnerId) : null,
status: "planned",
notes: onboardNotes.trim() ? onboardNotes : null,
},
})
}
disabled={!onboardAgentName.trim() || !onboardRole.trim() || !onboardPrompt.trim() || createOnboarding.isPending}
>
Create onboarding
</Button>
</div>
<div>
<div className="mb-2 text-sm font-medium">Current onboardings</div>
<ul className="space-y-2">
{(onboarding.data ?? []).map((o) => (
<li key={String(o.id)} className="rounded-md border p-3 text-sm">
<div className="font-medium">{o.agent_name} · {o.role_title}</div>
<div className="text-xs text-muted-foreground">status: {o.status} · cron: {o.cron_interval_ms ?? "—"}</div>
<div className="mt-2 grid gap-2">
<Select
value={o.status ?? ""}
onChange={(e) =>
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { status: e.target.value || null } })
}
>
<option value="planned">planned</option>
<option value="spawning">spawning</option>
<option value="spawned">spawned</option>
<option value="verified">verified</option>
<option value="blocked">blocked</option>
</Select>
<Input
placeholder="Spawned agent id"
defaultValue={o.spawned_agent_id ?? ""}
onBlur={(e) =>
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { spawned_agent_id: e.currentTarget.value || null } })
}
/>
<Input
placeholder="Session key"
defaultValue={o.session_key ?? ""}
onBlur={(e) =>
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { session_key: e.currentTarget.value || null } })
}
/>
<Textarea
placeholder="Notes"
defaultValue={o.notes ?? ""}
onBlur={(e) =>
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { notes: e.currentTarget.value || null } })
}
/>
</div>
</li>
))}
{(onboarding.data ?? []).length === 0 ? (
<li className="text-sm text-muted-foreground">No onboarding records yet.</li>
) : null}
</ul>
</div>
</CardContent>
</Card>
</div>
</div> </div>
</main> </main>
); );