diff --git a/heicode/controller/agent_template_handlers.go b/heicode/controller/agent_template_handlers.go index fc078900..9dcf1733 100644 --- a/heicode/controller/agent_template_handlers.go +++ b/heicode/controller/agent_template_handlers.go @@ -20,6 +20,34 @@ import ( // the subdomain directly over SSE. HM is NOT in the agent conversation path. // ───────────────────────────────────────────────────────────────────────────── +// sealAgentToken encrypts the AM-issued access token before it is persisted +// (AES-256-GCM via CryptoSecret). Falls back to plaintext only if CryptoSecret +// is unset, so the token is never lost. +func sealAgentToken(token string) string { + if strings.TrimSpace(token) == "" { + return "" + } + sealed, err := common.SealWithCryptoSecret([]byte(token)) + if err != nil { + common.SysLog("WARNING: agent access_token stored unencrypted (CryptoSecret unset?): " + err.Error()) + return token + } + return sealed +} + +// unsealAgentToken reverses sealAgentToken. If the stored value is not a sealed +// blob (legacy plaintext / CryptoSecret unset), it is returned as-is. +func unsealAgentToken(stored string) string { + if strings.TrimSpace(stored) == "" { + return "" + } + plain, err := common.UnsealWithCryptoSecret(stored) + if err != nil { + return stored + } + return string(plain) +} + // templateAgentResponse maps a deployed template-agent record to the client view. func templateAgentResponse(row model.AgentDeployment) gin.H { var bindingIDs []int @@ -30,7 +58,7 @@ func templateAgentResponse(row model.AgentDeployment) gin.H { "agent_id": row.DeploymentID, "template_id": row.TemplateID, "subdomain": row.Subdomain, - "access_token": row.AccessToken, + "access_token": unsealAgentToken(row.AccessToken), "binding_ids": bindingIDs, "status": row.Status, "runtime_id": row.RuntimeDeploymentID, @@ -110,7 +138,7 @@ func HeicodeDeployAgent(c *gin.Context) { UserID: strconv.Itoa(userID), TemplateID: req.TemplateID, Subdomain: result.Subdomain, - AccessToken: result.AccessToken, + AccessToken: sealAgentToken(result.AccessToken), BindingIDsJSON: string(bindingIDsJSON), RuntimeDeploymentID: result.RuntimeID, Status: firstNonEmpty(result.Status, "running"), diff --git a/heicode/controller/agent_template_test.go b/heicode/controller/agent_template_test.go index 1ac54b31..47227ee5 100644 --- a/heicode/controller/agent_template_test.go +++ b/heicode/controller/agent_template_test.go @@ -8,6 +8,7 @@ import ( "sync" "testing" + "github.com/heicode/manager/common" "github.com/heicode/manager/model" "github.com/stretchr/testify/require" ) @@ -197,6 +198,20 @@ func TestSeedAndLoadAgentTemplate(t *testing.T) { require.Equal(t, int64(19), count) // all presets seeded } +func TestAgentTokenSealRoundTrip(t *testing.T) { + old := common.CryptoSecret + common.CryptoSecret = "unit-test-crypto-secret" + defer func() { common.CryptoSecret = old }() + + sealed := sealAgentToken("super-secret-agent-token") + require.NotEqual(t, "super-secret-agent-token", sealed) // encrypted at rest + require.NotEmpty(t, sealed) + require.Equal(t, "super-secret-agent-token", unsealAgentToken(sealed)) // recovered + + require.Equal(t, "", sealAgentToken("")) + require.Equal(t, "plain-legacy", unsealAgentToken("plain-legacy")) // non-sealed passes through +} + func TestTemplateAgentResponse(t *testing.T) { row := model.AgentDeployment{ DeploymentID: "dep_abc",