feat(agent): template-agent deploy/list/stop API (AM contract isolated)
HM-side logic for the template-agent model, written against a proposed AM contract that is fully isolated in agent_template_runtime.go — when AM ships its real API, only the default paths + response field names in that one file change. - agent_template_runtime.go: AM adapter (list templates, start template agent with env, stop/delete) reusing the existing runtime config/url/envelope helpers. - agent_template_handlers.go: GET /api/heicode/agent-templates; POST /api/heicode/ agents (resolve bindings -> env, start via AM, persist subdomain+token); GET /agents, GET/:id, POST/:id/stop, DELETE/:id. Owner-scoped; env never logged. - reuses AgentDeployment as the agent record (TemplateID/Subdomain/AccessToken/ BindingIDsJSON) and buildAgentEnvFromBindings for env assembly. - routes wired under /api/heicode (UserOrV2DeviceAuth), verified no registration panic (router tests pass). - agent_template_test.go: 7 independent unit tests (env assembly metadata-only, ownership, secret-without-ref, unknown-source, empty; path substitution; response mapping) — all pass without AM/KV. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/heicode/manager/common"
|
||||
"github.com/heicode/manager/model"
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Template-agent client API (new model).
|
||||
//
|
||||
// User deploys a template agent from the web console; AM starts it with the
|
||||
// selected bound resources injected as env and returns a unique subdomain +
|
||||
// access token. The desktop client reads the agent list here, then connects to
|
||||
// the subdomain directly over SSE. HM is NOT in the agent conversation path.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// templateAgentResponse maps a deployed template-agent record to the client view.
|
||||
func templateAgentResponse(row model.AgentDeployment) gin.H {
|
||||
var bindingIDs []int
|
||||
if strings.TrimSpace(row.BindingIDsJSON) != "" {
|
||||
_ = common.UnmarshalJsonStr(row.BindingIDsJSON, &bindingIDs)
|
||||
}
|
||||
return gin.H{
|
||||
"agent_id": row.DeploymentID,
|
||||
"template_id": row.TemplateID,
|
||||
"subdomain": row.Subdomain,
|
||||
"access_token": row.AccessToken,
|
||||
"binding_ids": bindingIDs,
|
||||
"status": row.Status,
|
||||
"runtime_id": row.RuntimeDeploymentID,
|
||||
"created_at": row.CreatedAtText,
|
||||
"updated_at": row.UpdatedAtText,
|
||||
}
|
||||
}
|
||||
|
||||
// HeicodeListAgentTemplates: GET /api/heicode/agent-templates
|
||||
func HeicodeListAgentTemplates(c *gin.Context) {
|
||||
templates, err := amListTemplates(c.Request.Context())
|
||||
if err != nil {
|
||||
agentError(c, "RUNTIME_UNAVAILABLE", "failed to list templates: "+err.Error())
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{"templates": templates, "total": len(templates)})
|
||||
}
|
||||
|
||||
// HeicodeDeployAgent: POST /api/heicode/agents
|
||||
// Body: {template_id, binding_ids:[...]}.
|
||||
func HeicodeDeployAgent(c *gin.Context) {
|
||||
userID := c.GetInt("id")
|
||||
if userID <= 0 {
|
||||
agentError(c, "POLICY_REJECTED", "authentication required")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
BindingIDs []int `json:"binding_ids"`
|
||||
}
|
||||
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
|
||||
agentError(c, "POLICY_REJECTED", "invalid request body")
|
||||
return
|
||||
}
|
||||
req.TemplateID = strings.TrimSpace(req.TemplateID)
|
||||
if req.TemplateID == "" {
|
||||
agentError(c, "POLICY_REJECTED", "template_id is required")
|
||||
return
|
||||
}
|
||||
if model.DB == nil {
|
||||
agentError(c, "DEPLOYMENT_PERSIST_FAILED", "database not initialised")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve selected bindings -> env (non-secret config + KV-resolved secrets).
|
||||
// NEVER log env: it can contain plaintext secrets.
|
||||
env, err := buildAgentEnvFromBindings(userID, req.BindingIDs)
|
||||
if err != nil {
|
||||
agentError(c, "RESOURCE_BINDING_INVALID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
deploymentID := "dep_" + common.GetUUID()[:12]
|
||||
bindingIDsJSON, _ := common.Marshal(req.BindingIDs)
|
||||
|
||||
// Ask AM to start the template agent with the env injected.
|
||||
result, err := amStartTemplateAgent(c.Request.Context(), req.TemplateID, deploymentID, env, agentRuntimeCallbackURL())
|
||||
if err != nil {
|
||||
agentError(c, "RUNTIME_UNAVAILABLE", "failed to start agent: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
now := agentNow()
|
||||
nowMs := time.Now().UnixMilli()
|
||||
row := model.AgentDeployment{
|
||||
DeploymentID: deploymentID,
|
||||
UserID: strconv.Itoa(userID),
|
||||
TemplateID: req.TemplateID,
|
||||
Subdomain: result.Subdomain,
|
||||
AccessToken: result.AccessToken,
|
||||
BindingIDsJSON: string(bindingIDsJSON),
|
||||
RuntimeDeploymentID: result.RuntimeID,
|
||||
Status: firstNonEmpty(result.Status, "running"),
|
||||
CreatedAtText: now,
|
||||
UpdatedAtText: now,
|
||||
CreatedAtMs: nowMs,
|
||||
UpdatedAtMs: nowMs,
|
||||
}
|
||||
if err := model.DB.Create(&row).Error; err != nil {
|
||||
common.SysLog("HeicodeDeployAgent persist: " + err.Error())
|
||||
agentError(c, "DEPLOYMENT_PERSIST_FAILED", "failed to persist agent")
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, templateAgentResponse(row))
|
||||
}
|
||||
|
||||
// findUserTemplateAgent loads a deployed template agent owned by the caller.
|
||||
func findUserTemplateAgent(c *gin.Context) (model.AgentDeployment, bool) {
|
||||
var row model.AgentDeployment
|
||||
userID := c.GetInt("id")
|
||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||
if userID <= 0 || deploymentID == "" || model.DB == nil {
|
||||
agentError(c, "POLICY_REJECTED", "deployment_id and authentication required")
|
||||
return row, false
|
||||
}
|
||||
if err := model.DB.Where("deployment_id = ? AND user_id = ?", deploymentID, strconv.Itoa(userID)).First(&row).Error; err != nil {
|
||||
agentError(c, "DEPLOYMENT_CONFLICT", "agent not found")
|
||||
return row, false
|
||||
}
|
||||
return row, true
|
||||
}
|
||||
|
||||
// HeicodeListAgents: GET /api/heicode/agents
|
||||
func HeicodeListAgents(c *gin.Context) {
|
||||
userID := c.GetInt("id")
|
||||
if userID <= 0 || model.DB == nil {
|
||||
agentError(c, "POLICY_REJECTED", "authentication required")
|
||||
return
|
||||
}
|
||||
var rows []model.AgentDeployment
|
||||
// Only template-agent records (TemplateID set), not legacy task deployments.
|
||||
if err := model.DB.Where("user_id = ? AND template_id <> ''", strconv.Itoa(userID)).
|
||||
Order("created_at_ms desc").Find(&rows).Error; err != nil {
|
||||
agentError(c, "DEPLOYMENT_CONFLICT", "failed to list agents")
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, templateAgentResponse(row))
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
// HeicodeGetAgent: GET /api/heicode/agents/:deployment_id
|
||||
func HeicodeGetAgent(c *gin.Context) {
|
||||
row, ok := findUserTemplateAgent(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, templateAgentResponse(row))
|
||||
}
|
||||
|
||||
// HeicodeStopAgent: POST /api/heicode/agents/:deployment_id/stop
|
||||
func HeicodeStopAgent(c *gin.Context) {
|
||||
row, ok := findUserTemplateAgent(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(row.RuntimeDeploymentID) != "" {
|
||||
if err := amStopTemplateAgent(c.Request.Context(), row.RuntimeDeploymentID); err != nil {
|
||||
agentError(c, "RUNTIME_UNAVAILABLE", "failed to stop agent: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
row.Status = "stopped"
|
||||
row.UpdatedAtText = agentNow()
|
||||
row.UpdatedAtMs = time.Now().UnixMilli()
|
||||
if err := model.DB.Save(&row).Error; err != nil {
|
||||
agentError(c, "DEPLOYMENT_PERSIST_FAILED", "failed to persist stop")
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, templateAgentResponse(row))
|
||||
}
|
||||
|
||||
// HeicodeDeleteAgent: DELETE /api/heicode/agents/:deployment_id
|
||||
func HeicodeDeleteAgent(c *gin.Context) {
|
||||
row, ok := findUserTemplateAgent(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(row.RuntimeDeploymentID) != "" {
|
||||
if err := amDeleteTemplateAgent(c.Request.Context(), row.RuntimeDeploymentID); err != nil {
|
||||
agentError(c, "RUNTIME_UNAVAILABLE", "failed to delete agent: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := model.DB.Delete(&row).Error; err != nil {
|
||||
agentError(c, "DEPLOYMENT_PERSIST_FAILED", "failed to delete agent")
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{"deployment_id": row.DeploymentID, "status": "deleted"})
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/heicode/manager/common"
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// AM template-agent adapter (the ONLY place that knows AM's template HTTP API).
|
||||
//
|
||||
// Template-agent model: HM asks AM to start a template agent with the selected
|
||||
// resources injected as env; AM returns a unique subdomain + access token; the
|
||||
// desktop client then talks to that subdomain directly over SSE.
|
||||
//
|
||||
// When AM finalises its real API, change ONLY:
|
||||
// - the default paths in the *Path() helpers (or set the env overrides), and
|
||||
// - the request payload / response field names in the three am* functions.
|
||||
// No other HM code needs to change.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// agentTemplate is one deployable template offered by AM.
|
||||
type agentTemplate struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RequiredResourceTypes []string `json:"required_resource_types"`
|
||||
EnvSchema []string `json:"env_schema"`
|
||||
}
|
||||
|
||||
// amStartResult is what AM returns after starting a template agent.
|
||||
type amStartResult struct {
|
||||
RuntimeID string
|
||||
Subdomain string
|
||||
AccessToken string
|
||||
Status string
|
||||
}
|
||||
|
||||
func agentTemplatesPath() string {
|
||||
return common.GetEnvOrDefaultString("AGENT_RUNTIME_TEMPLATES_PATH", "/api/agent/templates")
|
||||
}
|
||||
|
||||
func agentTemplateStartPath(templateID string) string {
|
||||
p := common.GetEnvOrDefaultString("AGENT_RUNTIME_TEMPLATE_START_PATH", "/api/agent/templates/{template_id}/start")
|
||||
return strings.ReplaceAll(p, "{template_id}", url.PathEscape(templateID))
|
||||
}
|
||||
|
||||
func agentTemplateAgentPath(agentRuntimeID string) string {
|
||||
p := common.GetEnvOrDefaultString("AGENT_RUNTIME_AGENT_PATH", "/api/agent/agents/{agent_id}")
|
||||
return strings.ReplaceAll(p, "{agent_id}", url.PathEscape(agentRuntimeID))
|
||||
}
|
||||
|
||||
func agentTemplateAgentStopPath(agentRuntimeID string) string {
|
||||
p := common.GetEnvOrDefaultString("AGENT_RUNTIME_AGENT_STOP_PATH", "/api/agent/agents/{agent_id}/stop")
|
||||
return strings.ReplaceAll(p, "{agent_id}", url.PathEscape(agentRuntimeID))
|
||||
}
|
||||
|
||||
// amTemplateDo performs an AM HTTP call and returns the decoded `data` object.
|
||||
// Reuses the existing runtime config (base URL / service token / timeout).
|
||||
func amTemplateDo(ctx context.Context, method, path string, body any) (map[string]any, error) {
|
||||
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeAgent)
|
||||
endpoint, err := agentRuntimeURL(cfg.BaseURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
raw, err := common.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.Token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.Token)
|
||||
}
|
||||
client := &http.Client{Timeout: cfg.Timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("AM %s %s returned HTTP %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
}
|
||||
var envelope map[string]any
|
||||
if len(raw) > 0 {
|
||||
if err := common.Unmarshal(raw, &envelope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if msg := agentRuntimeEnvelopeError(envelope); msg != "" {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
return extractAgentRuntimeData(envelope), nil
|
||||
}
|
||||
|
||||
// amListTemplates lists AM's deployable templates.
|
||||
// Proposed contract: data.templates = [...]. Swap this mapping if AM differs.
|
||||
func amListTemplates(ctx context.Context) ([]agentTemplate, error) {
|
||||
data, err := amTemplateDo(ctx, http.MethodGet, agentTemplatesPath(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := common.Marshal(data["templates"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var templates []agentTemplate
|
||||
if err := common.Unmarshal(raw, &templates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
// amStartTemplateAgent asks AM to start a template agent with the given env.
|
||||
// Proposed request: {manager_deployment_id, env, callback_url}.
|
||||
// Proposed response data: {runtime_id, subdomain, access_token, status}.
|
||||
func amStartTemplateAgent(ctx context.Context, templateID, managerDeploymentID string, env map[string]string, callbackURL string) (amStartResult, error) {
|
||||
payload := map[string]any{
|
||||
"manager_deployment_id": managerDeploymentID,
|
||||
"env": env,
|
||||
"callback_url": callbackURL,
|
||||
}
|
||||
data, err := amTemplateDo(ctx, http.MethodPost, agentTemplateStartPath(templateID), payload)
|
||||
if err != nil {
|
||||
return amStartResult{}, err
|
||||
}
|
||||
return amStartResult{
|
||||
RuntimeID: stringFromMap(data, "runtime_id", "agent_id", "id", "deployment_id"),
|
||||
Subdomain: stringFromMap(data, "subdomain", "address", "url"),
|
||||
AccessToken: stringFromMap(data, "access_token", "token"),
|
||||
Status: stringFromMap(data, "status", "runtime_status"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func amStopTemplateAgent(ctx context.Context, runtimeID string) error {
|
||||
_, err := amTemplateDo(ctx, http.MethodPost, agentTemplateAgentStopPath(runtimeID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func amDeleteTemplateAgent(ctx context.Context, runtimeID string) error {
|
||||
_, err := amTemplateDo(ctx, http.MethodDelete, agentTemplateAgentPath(runtimeID), nil)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/heicode/manager/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildAgentEnvFromBindings_MetadataOnly(t *testing.T) {
|
||||
setupResourceControllerTestDB(t)
|
||||
|
||||
b := model.ResourceBinding{
|
||||
UserId: 7,
|
||||
Name: "blob1",
|
||||
ResourceType: "blob",
|
||||
Metadata: `{"account":"myacct","container":"uploads"}`,
|
||||
EnvMap: `{"AZURE_BLOB_ACCOUNT":{"source":"metadata","key":"account"},"AZURE_BLOB_CONTAINER":{"source":"metadata","key":"container"}}`,
|
||||
}
|
||||
require.NoError(t, model.DB.Create(&b).Error)
|
||||
|
||||
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "myacct", env["AZURE_BLOB_ACCOUNT"])
|
||||
require.Equal(t, "uploads", env["AZURE_BLOB_CONTAINER"])
|
||||
}
|
||||
|
||||
func TestBuildAgentEnvFromBindings_OwnershipEnforced(t *testing.T) {
|
||||
setupResourceControllerTestDB(t)
|
||||
b := model.ResourceBinding{UserId: 7, Name: "x", ResourceType: "blob", Metadata: `{}`, EnvMap: `{}`}
|
||||
require.NoError(t, model.DB.Create(&b).Error)
|
||||
|
||||
_, err := buildAgentEnvFromBindings(99, []int{b.Id}) // different user
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBuildAgentEnvFromBindings_SecretWithoutRefFailsBeforeKV(t *testing.T) {
|
||||
setupResourceControllerTestDB(t)
|
||||
b := model.ResourceBinding{
|
||||
UserId: 7,
|
||||
Name: "vm1",
|
||||
ResourceType: "vm",
|
||||
Metadata: `{"host":"1.2.3.4"}`,
|
||||
EnvMap: `{"VM_PASSWORD":{"source":"secret","key":"password"}}`,
|
||||
// SecretRef intentionally empty -> must error before any Key Vault call
|
||||
}
|
||||
require.NoError(t, model.DB.Create(&b).Error)
|
||||
|
||||
_, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "secret_ref")
|
||||
}
|
||||
|
||||
func TestBuildAgentEnvFromBindings_UnknownSource(t *testing.T) {
|
||||
setupResourceControllerTestDB(t)
|
||||
b := model.ResourceBinding{
|
||||
UserId: 7,
|
||||
Name: "x",
|
||||
ResourceType: "blob",
|
||||
Metadata: `{}`,
|
||||
EnvMap: `{"FOO":{"source":"bogus","key":"x"}}`,
|
||||
}
|
||||
require.NoError(t, model.DB.Create(&b).Error)
|
||||
|
||||
_, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBuildAgentEnvFromBindings_Empty(t *testing.T) {
|
||||
setupResourceControllerTestDB(t)
|
||||
env, err := buildAgentEnvFromBindings(7, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, env)
|
||||
}
|
||||
|
||||
func TestAgentTemplatePathSubstitution(t *testing.T) {
|
||||
require.Contains(t, agentTemplateStartPath("tpl1"), "/templates/tpl1/start")
|
||||
require.Contains(t, agentTemplateAgentStopPath("rt-9"), "/agents/rt-9/stop")
|
||||
require.Contains(t, agentTemplateAgentPath("rt-9"), "/agents/rt-9")
|
||||
}
|
||||
|
||||
func TestTemplateAgentResponse(t *testing.T) {
|
||||
row := model.AgentDeployment{
|
||||
DeploymentID: "dep_abc",
|
||||
TemplateID: "tpl1",
|
||||
Subdomain: "https://abc.agents.example",
|
||||
AccessToken: "tok",
|
||||
BindingIDsJSON: "[1,2]",
|
||||
Status: "running",
|
||||
}
|
||||
resp := templateAgentResponse(row)
|
||||
require.Equal(t, "dep_abc", resp["agent_id"])
|
||||
require.Equal(t, "tpl1", resp["template_id"])
|
||||
require.Equal(t, []int{1, 2}, resp["binding_ids"])
|
||||
require.Equal(t, "running", resp["status"])
|
||||
}
|
||||
@@ -566,6 +566,20 @@ func SetApiRouter(router *gin.Engine) {
|
||||
heicodeSwarmRoute.Use(middleware.UserOrV2DeviceAuth())
|
||||
registerHeicodeTaskRoutes(heicodeSwarmRoute, controller.HeicodeCreateSwarmTask, controller.HeicodeListSwarmTasks)
|
||||
|
||||
// Template-agent model (new): deploy a template agent with bound resources
|
||||
// injected as env; client reads the agent list and connects to the agent's
|
||||
// subdomain directly. Same V2-device / session auth as the task routes.
|
||||
heicodeAgentRoute := apiRouter.Group("/heicode")
|
||||
heicodeAgentRoute.Use(middleware.UserOrV2DeviceAuth())
|
||||
{
|
||||
heicodeAgentRoute.GET("/agent-templates", controller.HeicodeListAgentTemplates)
|
||||
heicodeAgentRoute.POST("/agents", controller.HeicodeDeployAgent)
|
||||
heicodeAgentRoute.GET("/agents", controller.HeicodeListAgents)
|
||||
heicodeAgentRoute.GET("/agents/:deployment_id", controller.HeicodeGetAgent)
|
||||
heicodeAgentRoute.POST("/agents/:deployment_id/stop", controller.HeicodeStopAgent)
|
||||
heicodeAgentRoute.DELETE("/agents/:deployment_id", controller.HeicodeDeleteAgent)
|
||||
}
|
||||
|
||||
// Agent orchestration control plane (minimal integration endpoints)
|
||||
agentRoute := apiRouter.Group("/agent")
|
||||
agentRoute.Use(middleware.AdminAuth())
|
||||
|
||||
Reference in New Issue
Block a user