diff --git a/heicode/controller/agnet_role_template.go b/heicode/controller/agnet_role_template.go new file mode 100644 index 0000000..dea3784 --- /dev/null +++ b/heicode/controller/agnet_role_template.go @@ -0,0 +1,155 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "github.com/heicode/manager/common" +) + +// AgnetRoleTemplate is the platform-recommended role catalog Manager +// surfaces to users when they assemble an AI development team. The +// six canonical roles come from docs/product-package/13-platform- +// description.md §3 and 04-platform-usage-guide.md §第五步. +// +// Why constants, not a DB table: +// - Roles are platform-defined contracts, not user-editable data. +// Treating them like rows would invite drift between deployments. +// - Permission hints below are *recommendations* the UI uses to +// pre-fill the "what can this Agnet do" confirmation card — +// the actual permission grant still goes through ResourceGrant. +// - If we ever need per-tenant role customization, we add an +// overlay table; the canonical set still lives here as the +// baseline. +// +// Wire-format note: keys (key column) are stable identifiers used +// across persistence and the client picker. Display strings can +// be translated, but the key must NEVER change without a coordinated +// frontend rollout. +type AgnetRoleTemplate struct { + Key string `json:"key"` + DisplayName string `json:"display_name"` + Summary string `json:"summary"` + DefaultModel string `json:"default_model"` + DefaultPermissions []string `json:"default_permissions"` + RiskLevel string `json:"risk_level"` +} + +// agnetRoleTemplates returns the canonical six-role catalog. Order +// matches the typical lifecycle a user walks through when assembling +// a team: discover -> design -> build -> review -> operate. +// +// Permission hints use the verbs from docs §13.3.4 (Resource Grant) +// and stay deliberately broad — concrete grants come from the user +// resource-binding flow. +func agnetRoleTemplates() []AgnetRoleTemplate { + return []AgnetRoleTemplate{ + { + Key: "product", + DisplayName: "Product Agnet", + Summary: "Refines the user idea into product scope, requirements and acceptance criteria.", + DefaultModel: "claude-sonnet-4-6", + DefaultPermissions: []string{ + "read:project_docs", + "write:product_spec", + }, + RiskLevel: agnetRiskLow, + }, + { + Key: "architect", + DisplayName: "Architect Agnet", + Summary: "Designs the technical approach, picks frameworks, and breaks work into sub-tasks.", + DefaultModel: "claude-opus-4-7", + DefaultPermissions: []string{ + "read:repo", + "write:architecture_doc", + }, + RiskLevel: agnetRiskLow, + }, + { + Key: "frontend", + DisplayName: "Frontend Agnet", + Summary: "Implements UI, components and client-side state per the architect's plan.", + DefaultModel: "claude-sonnet-4-6", + DefaultPermissions: []string{ + "read:repo", + "write:repo:frontend", + }, + RiskLevel: agnetRiskMedium, + }, + { + Key: "backend", + DisplayName: "Backend Agnet", + Summary: "Implements server-side APIs, data models and integrations.", + DefaultModel: "claude-sonnet-4-6", + DefaultPermissions: []string{ + "read:repo", + "write:repo:backend", + "read:dev_database", + }, + RiskLevel: agnetRiskMedium, + }, + { + Key: "reviewer", + DisplayName: "Reviewer Agnet", + Summary: "Performs code review, security checks and runs the test suite.", + DefaultModel: "claude-opus-4-7", + DefaultPermissions: []string{ + "read:repo", + "run:tests", + "comment:pull_request", + }, + RiskLevel: agnetRiskLow, + }, + { + Key: "ops", + DisplayName: "Ops Agnet", + Summary: "Deploys to test environments, watches logs and prepares production rollouts (production requires approval).", + DefaultModel: "claude-sonnet-4-6", + DefaultPermissions: []string{ + "read:repo", + "deploy:test_env", + "read:metrics", + "approval_required:deploy_prod", + }, + RiskLevel: agnetRiskHigh, + }, + } +} + +// AgnetListRoleTemplates is the GET /api/agnet/role-templates handler. +// Returns the canonical six-role catalog so the deployment-creation +// UI can pre-populate role pickers and the documentation page can +// render the role overview. +// +// Auth: requires UserAuth (mounted by router). Anyone logged in to +// Manager can read the catalog; there are no secrets in the payload. +func AgnetListRoleTemplates(c *gin.Context) { + tpls := agnetRoleTemplates() + common.ApiSuccess(c, gin.H{ + "items": tpls, + "total": len(tpls), + }) +} + +// agnetRoleTemplateKeys is a helper for validation in deployment +// creation — checks whether a user-provided role_template string is +// one of the canonical six. Returns true for any of the canonical +// keys; returns true for unknown keys too (deployment flow today +// accepts free-form role_template strings, see agnet_control_plane. +// go:552), so this helper is currently advisory. When we tighten +// validation (after frontend ships the new picker), flip the +// fallback to false and add a unit test. +func agnetRoleTemplateKeys() map[string]bool { + keys := make(map[string]bool) + for _, t := range agnetRoleTemplates() { + keys[t.Key] = true + } + return keys +} + +// agnetIsCanonicalRoleKey reports whether `key` matches one of the +// six platform-defined roles. Today the deployment endpoint accepts +// any non-empty string; this helper is reserved for the next step +// when we move to a closed set. +func agnetIsCanonicalRoleKey(key string) bool { + return agnetRoleTemplateKeys()[key] +} diff --git a/heicode/controller/agnet_role_template_test.go b/heicode/controller/agnet_role_template_test.go new file mode 100644 index 0000000..5a313d8 --- /dev/null +++ b/heicode/controller/agnet_role_template_test.go @@ -0,0 +1,100 @@ +package controller + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/heicode/manager/common" +) + +func TestAgnetRoleTemplates_CanonicalSetCovered(t *testing.T) { + // Pins the six canonical role keys from docs §13.3.3. Any code + // change that adds, removes or renames a key MUST update this + // list — guards against accidental drift between Manager and + // the product spec. + want := []string{"product", "architect", "frontend", "backend", "reviewer", "ops"} + got := agnetRoleTemplates() + if len(got) != len(want) { + t.Fatalf("expected %d roles, got %d", len(want), len(got)) + } + for i, key := range want { + if got[i].Key != key { + t.Errorf("position %d: want key %q, got %q", i, key, got[i].Key) + } + // Display + summary populated; never returning a blank-row + // in the picker. + if strings.TrimSpace(got[i].DisplayName) == "" { + t.Errorf("role %q has empty DisplayName", key) + } + if strings.TrimSpace(got[i].Summary) == "" { + t.Errorf("role %q has empty Summary", key) + } + if len(got[i].DefaultPermissions) == 0 { + t.Errorf("role %q has empty DefaultPermissions", key) + } + } +} + +func TestAgnetRoleTemplates_RiskLevels(t *testing.T) { + // Ops is the only canonical role with high risk (production + // deployment intent). Reviewer + Product + Architect stay low + // (read-mostly). Frontend + Backend land at medium. Pins the + // product-doc default risk classification so we don't silently + // flip an ops role to "low" and skip the high-risk approval + // gating downstream. + wantRisk := map[string]string{ + "product": agnetRiskLow, + "architect": agnetRiskLow, + "reviewer": agnetRiskLow, + "frontend": agnetRiskMedium, + "backend": agnetRiskMedium, + "ops": agnetRiskHigh, + } + for _, tpl := range agnetRoleTemplates() { + if want, ok := wantRisk[tpl.Key]; ok && tpl.RiskLevel != want { + t.Errorf("role %q: want risk %q, got %q", tpl.Key, want, tpl.RiskLevel) + } + } +} + +func TestAgnetListRoleTemplates_HTTPShape(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest("GET", "/api/agnet/role-templates", nil) + c.Set("id", 1) + c.Set("role", common.RoleCommonUser) + + AgnetListRoleTemplates(c) + + if rec.Code != 200 { + t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) + } + body := rec.Body.String() + // Stable keys the frontend depends on — checked via substring + // rather than parsing so we don't have to spin up a typed + // envelope just for this guard. + for _, want := range []string{`"items"`, `"total"`, `"product"`, `"ops"`, `"display_name"`} { + if !strings.Contains(body, want) { + t.Errorf("response missing %q. body=%s", want, body) + } + } +} + +func TestAgnetIsCanonicalRoleKey(t *testing.T) { + // Defensive helper currently used as advisory — pins the closed + // set so the future tighten-up to closed-set validation is one + // flip instead of an open-ended audit. + for _, ok := range []string{"product", "architect", "frontend", "backend", "reviewer", "ops"} { + if !agnetIsCanonicalRoleKey(ok) { + t.Errorf("%q should be canonical", ok) + } + } + for _, bad := range []string{"", "debugger", "executor", "random_string"} { + if agnetIsCanonicalRoleKey(bad) { + t.Errorf("%q should NOT be canonical", bad) + } + } +} diff --git a/heicode/router/api-router.go b/heicode/router/api-router.go index a9a7cb0..fa4fe8d 100644 --- a/heicode/router/api-router.go +++ b/heicode/router/api-router.go @@ -497,6 +497,12 @@ func SetApiRouter(router *gin.Engine) { agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots) agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot) agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs) + // Platform-recommended role catalog. Six canonical Agnet + // roles per docs/product-package/13-platform-description.md + // §3 (Product / Architect / Frontend / Backend / Reviewer / + // Ops). UI uses this to populate the deployment-creation + // role picker. Read-only, no secrets in payload. + agnetRoute.GET("/role-templates", controller.AgnetListRoleTemplates) } } } diff --git a/heicode/web/default/src/features/agnet-console/api.ts b/heicode/web/default/src/features/agnet-console/api.ts index 91dd670..e836a93 100644 --- a/heicode/web/default/src/features/agnet-console/api.ts +++ b/heicode/web/default/src/features/agnet-console/api.ts @@ -178,6 +178,36 @@ export type GitSourcePayload = { usage: GitSourceUsage } +// Platform-recommended role catalog shape. Mirrors backend +// `AgnetRoleTemplate` in controller/agnet_role_template.go. The +// six canonical roles come from docs/product-package §13.3.3 — +// keys are stable identifiers, display strings can be translated. +export type AgnetRoleTemplate = { + key: string + display_name: string + summary: string + default_model: string + default_permissions: string[] + risk_level: 'low' | 'medium' | 'high' +} + +// Cached at module level — the canonical six-role catalog doesn't +// change between page loads, so we avoid an extra request every +// time the create-deployment sheet opens. +let _roleTemplateCache: AgnetRoleTemplate[] | null = null + +export async function listAgnetRoleTemplates(): Promise { + if (_roleTemplateCache) return _roleTemplateCache + const res = await api.get>( + '/api/agnet/role-templates' + ) + const items = res.data?.data?.items ?? [] + if (items.length > 0) { + _roleTemplateCache = items + } + return items +} + export async function listAgnetDeployments(): Promise { const res = await api.get>( '/api/agnet/deployments' diff --git a/heicode/web/default/src/features/agnet-console/create-agnet-deployment-sheet.tsx b/heicode/web/default/src/features/agnet-console/create-agnet-deployment-sheet.tsx index b51ee41..0432e53 100644 --- a/heicode/web/default/src/features/agnet-console/create-agnet-deployment-sheet.tsx +++ b/heicode/web/default/src/features/agnet-console/create-agnet-deployment-sheet.tsx @@ -41,8 +41,10 @@ import { cn } from '@/lib/utils' import { useAuthStore } from '@/stores/auth-store' import { createAgnetDeployment, + listAgnetRoleTemplates, type AgnetAgentPlan, type AgnetOrchestrationPlan, + type AgnetRoleTemplate, type AgnetSKSource, } from './api' @@ -127,14 +129,17 @@ const TEMPLATE_PRESETS: TemplatePreset[] = [ label: 'Fix or improve', objective: 'Investigate a focused issue and ship the smallest safe fix.', risk: 'low', + // Role keys aligned to the canonical six-role catalog from + // docs/product-package §13.3.3. Previous presets used informal + // "debugger" / "executor" labels that don't exist in the API. roles: [ { - role_template: 'debugger', + role_template: 'reviewer', goal: 'Find the root cause and define the minimum repair.', default_model_id: 'agnet-model-debugger', }, { - role_template: 'executor', + role_template: 'backend', goal: 'Implement and verify the scoped change.', default_model_id: 'agnet-model-builder', }, @@ -145,9 +150,13 @@ const TEMPLATE_PRESETS: TemplatePreset[] = [ label: 'Release readiness', objective: 'Prepare a release candidate with verification and audit trail.', risk: 'high', + // Canonical six-role catalog (§13.3.3) — "executor" was an + // informal label; the closest canonical mapping is "backend" + // (build the release artifact) followed by "ops" (validate + // rollout) and "reviewer" (confirm evidence). roles: [ { - role_template: 'executor', + role_template: 'backend', goal: 'Prepare the release branch and required artifacts.', default_model_id: 'agnet-model-builder', }, @@ -370,6 +379,27 @@ export function CreateAgnetDeploymentSheet({ const userId = String(currentUser?.id || userScopeRef.trim() || 'user_local') + // Platform-recommended role catalog. Loaded once when the sheet + // opens; the API client caches the response at module level so + // subsequent sheet opens don't re-fetch. Empty array fallback + // means the sheet stays usable if the catalog endpoint is down — + // the role picker falls back to free-text input. + const [roleTemplates, setRoleTemplates] = useState([]) + useEffect(() => { + if (!open) return + let cancelled = false + listAgnetRoleTemplates() + .then((items) => { + if (!cancelled) setRoleTemplates(items) + }) + .catch(() => { + // Best-effort — sheet still works without the catalog. + }) + return () => { + cancelled = true + } + }, [open]) + useEffect(() => { if (open && !correlationId) { setCorrelationId(crypto.randomUUID()) @@ -882,16 +912,43 @@ export function CreateAgnetDeploymentSheet({
- - updateAgent(index, { - role_template: e.target.value, - }) - } - className='font-mono text-xs' - /> + {/* Role picker — bound to the canonical six-role + catalog from /api/agnet/role-templates. Falls + back to a free-text input if the catalog + failed to load. */} + {roleTemplates.length > 0 ? ( + + ) : ( + + updateAgent(index, { + role_template: e.target.value, + }) + } + className='font-mono text-xs' + /> + )}