Files
dingtalk-workspace-cli/internal/auth/identity.go
T
2026-03-27 17:55:57 +08:00

115 lines
3.2 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// identity.go manages agent instance identification for tracking.
//
// Each agent installation gets a unique agentId (UUID v4) that persists across
// version upgrades but regenerates on reinstall. This identity is transparently
// injected into MCP HTTP headers for gateway-side data collection.
package auth
import (
"crypto/rand"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/config"
)
const identityFile = "identity.json"
// Identity holds the agent instance identification fields.
type Identity struct {
AgentID string `json:"agentId"` // UUID v4, generated at install time
Source string `json:"source"` // data source, default "dws"
}
// Load reads the identity from <configDir>/identity.json.
// Returns nil if the file does not exist or cannot be parsed.
func Load(configDir string) *Identity {
path := filepath.Join(configDir, identityFile)
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var id Identity
if err := json.Unmarshal(data, &id); err != nil {
return nil
}
if id.AgentID == "" {
return nil
}
return &id
}
// EnsureExists loads existing identity or creates a new one if not present.
func EnsureExists(configDir string) *Identity {
if id := Load(configDir); id != nil {
return id
}
id := &Identity{
AgentID: generateUUID(),
Source: "dws",
}
// Best-effort persist — don't fail the CLI if write fails.
_ = save(configDir, id)
return id
}
// Headers returns the identity as HTTP header key-value pairs.
func (id *Identity) Headers() map[string]string {
if id == nil {
return nil
}
h := make(map[string]string, 5)
if id.AgentID != "" {
h["x-dws-agent-id"] = id.AgentID
}
if id.Source != "" {
h["x-dws-source"] = id.Source
}
// Constant headers for MCP gateway tracking
h["x-dingtalk-scenario-code"] = "com.dingtalk.cli"
h["x-dingtalk-source"] = "github"
return h
}
func save(configDir string, id *Identity) error {
if err := os.MkdirAll(configDir, config.DirPerm); err != nil {
return err
}
data, err := json.MarshalIndent(id, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
return os.WriteFile(filepath.Join(configDir, identityFile), data, config.FilePerm)
}
// generateUUID produces a UUID v4 string.
func generateUUID() string {
var u [16]byte
if _, err := rand.Read(u[:]); err != nil {
// Extremely unlikely; fallback to zero UUID rather than panic.
return "00000000-0000-4000-8000-000000000000"
}
u[6] = (u[6] & 0x0f) | 0x40 // version 4
u[8] = (u[8] & 0x3f) | 0x80 // variant 10
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
u[0:4], u[4:6], u[6:8], u[8:10], u[10:16])
}