| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- package service
- import (
- "context"
- "crypto/ed25519"
- "crypto/rand"
- "crypto/sha512"
- "crypto/x509"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/stretchr/testify/require"
- "golang.org/x/crypto/curve25519"
- "golang.org/x/crypto/nacl/box"
- )
- func newTestAgentIdentityKey(t *testing.T) (agentIdentityKey, string) {
- t.Helper()
- _, privateKey, err := ed25519.GenerateKey(rand.Reader)
- require.NoError(t, err)
- der, err := x509.MarshalPKCS8PrivateKey(privateKey)
- require.NoError(t, err)
- return agentIdentityKey{
- runtimeID: "runtime-test",
- privateKey: privateKey,
- taskID: "task-test",
- }, base64.StdEncoding.EncodeToString(der)
- }
- func TestBuildAgentAssertionMatchesCodexEnvelopeAndSignature(t *testing.T) {
- key, _ := newTestAgentIdentityKey(t)
- now := time.Date(2026, 7, 14, 8, 9, 10, 0, time.FixedZone("UTC+8", 8*60*60))
- assertion, err := buildAgentAssertion(key, now)
- require.NoError(t, err)
- require.True(t, strings.HasPrefix(assertion, "AgentAssertion "))
- encoded := strings.TrimPrefix(assertion, "AgentAssertion ")
- decoded, err := base64.RawURLEncoding.DecodeString(encoded)
- require.NoError(t, err)
- var envelope struct {
- AgentRuntimeID string `json:"agent_runtime_id"`
- TaskID string `json:"task_id"`
- Timestamp string `json:"timestamp"`
- Signature string `json:"signature"`
- }
- require.NoError(t, json.Unmarshal(decoded, &envelope))
- require.Equal(t, "runtime-test", envelope.AgentRuntimeID)
- require.Equal(t, "task-test", envelope.TaskID)
- require.Equal(t, "2026-07-14T00:09:10Z", envelope.Timestamp)
- signature, err := base64.StdEncoding.DecodeString(envelope.Signature)
- require.NoError(t, err)
- publicKey, ok := key.privateKey.Public().(ed25519.PublicKey)
- require.True(t, ok)
- require.True(t, ed25519.Verify(publicKey, []byte("runtime-test:task-test:2026-07-14T00:09:10Z"), signature))
- }
- func TestDecryptAgentTaskIDSupportsCodexSealedBoxResponse(t *testing.T) {
- key, _ := newTestAgentIdentityKey(t)
- digest := sha512.Sum512(key.privateKey.Seed())
- var curvePrivate [32]byte
- copy(curvePrivate[:], digest[:32])
- curvePrivate[0] &= 248
- curvePrivate[31] &= 127
- curvePrivate[31] |= 64
- curvePublicBytes, err := curve25519.X25519(curvePrivate[:], curve25519.Basepoint)
- require.NoError(t, err)
- var curvePublic [32]byte
- copy(curvePublic[:], curvePublicBytes)
- ciphertext, err := box.SealAnonymous(nil, []byte("task-sealed"), &curvePublic, rand.Reader)
- require.NoError(t, err)
- got, err := decryptAgentTaskID(key, base64.StdEncoding.EncodeToString(ciphertext))
- require.NoError(t, err)
- require.Equal(t, "task-sealed", got)
- }
- func TestRegisterAgentIdentityTaskAcceptsPlaintextAndEncryptedResponses(t *testing.T) {
- key, privateKey := newTestAgentIdentityKey(t)
- requestCount := 0
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- require.Equal(t, "/v1/agent/runtime-test/task/register", r.URL.Path)
- var request map[string]string
- require.NoError(t, json.NewDecoder(r.Body).Decode(&request))
- require.NotEmpty(t, request["timestamp"])
- require.NotEmpty(t, request["signature"])
- requestCount++
- if requestCount == 2 {
- digest := sha512.Sum512(key.privateKey.Seed())
- var curvePrivate [32]byte
- copy(curvePrivate[:], digest[:32])
- curvePrivate[0] &= 248
- curvePrivate[31] &= 127
- curvePrivate[31] |= 64
- curvePublicBytes, curveErr := curve25519.X25519(curvePrivate[:], curve25519.Basepoint)
- require.NoError(t, curveErr)
- var curvePublic [32]byte
- copy(curvePublic[:], curvePublicBytes)
- ciphertext, sealErr := box.SealAnonymous(nil, []byte("task-encrypted"), &curvePublic, rand.Reader)
- require.NoError(t, sealErr)
- _, _ = fmt.Fprintf(w, `{"encrypted_task_id":%q}`, base64.StdEncoding.EncodeToString(ciphertext))
- return
- }
- _, _ = w.Write([]byte(`{"task_id":"task-plain"}`))
- }))
- defer server.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = server.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- account := &Account{ID: 1, Type: AccountTypeOAuth, Platform: PlatformOpenAI, Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- }}
- taskID, err := registerAgentIdentityTask(context.Background(), account)
- require.NoError(t, err)
- require.Equal(t, "task-plain", taskID)
- taskID, err = registerAgentIdentityTask(context.Background(), account)
- require.NoError(t, err)
- require.Equal(t, "task-encrypted", taskID)
- }
- func TestEnsureAgentIdentityTaskPersistsAndRedactsCredentials(t *testing.T) {
- key, privateKey := newTestAgentIdentityKey(t)
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _ = w.Write([]byte(`{"task_id":"task-persisted"}`))
- }))
- defer server.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = server.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- repo := &agentIdentityCredentialsRepo{}
- account := &Account{ID: 7, Type: AccountTypeOAuth, Platform: PlatformOpenAI, Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "chatgpt_account_id": "account-test",
- }}
- service := &OpenAIGatewayService{accountRepo: repo}
- require.NoError(t, service.ensureAgentIdentityTask(context.Background(), account, ""))
- require.Equal(t, "task-persisted", account.GetCredential("task_id"))
- require.Equal(t, "task-persisted", repo.credentials["task_id"])
- require.True(t, IsSensitiveCredentialKey("agent_private_key"))
- redacted := make(map[string]any)
- for key, value := range account.Credentials {
- if !IsSensitiveCredentialKey(key) {
- redacted[key] = value
- }
- }
- require.NotContains(t, string(mustAgentIdentityJSON(t, redacted)), privateKey)
- }
- func TestEnsureAgentIdentityTaskSharesLockAcrossServicesForSameAccount(t *testing.T) {
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{ID: 9001, Type: AccountTypeOAuth, Platform: PlatformOpenAI, Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- }}
- repo := &agentIdentityCredentialsRepo{account: account}
- registerCalls := 0
- var registerMu sync.Mutex
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- registerMu.Lock()
- registerCalls++
- registerMu.Unlock()
- _, _ = w.Write([]byte(`{"task_id":"task-shared"}`))
- }))
- defer server.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = server.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- start := make(chan struct{})
- errors := make(chan error, 2)
- requests := []*Account{cloneAgentIdentityTestAccount(account), cloneAgentIdentityTestAccount(account)}
- for _, request := range requests {
- go func() {
- <-start
- errors <- ensureAgentIdentityTaskForAccount(context.Background(), repo, nil, &sync.Mutex{}, request, "")
- }()
- }
- close(start)
- require.NoError(t, <-errors)
- require.NoError(t, <-errors)
- registerMu.Lock()
- defer registerMu.Unlock()
- require.Equal(t, 1, registerCalls)
- require.Equal(t, "task-shared", repo.account.GetCredential("task_id"))
- }
- func cloneAgentIdentityTestAccount(account *Account) *Account {
- copy := *account
- copy.Credentials = shallowCopyMap(account.Credentials)
- return ©
- }
- type agentIdentityCredentialsRepo struct {
- AccountRepository
- credentials map[string]any
- account *Account
- mu sync.Mutex
- }
- func (r *agentIdentityCredentialsRepo) GetByID(_ context.Context, _ int64) (*Account, error) {
- return r.account, nil
- }
- func (r *agentIdentityCredentialsRepo) UpdateCredentials(_ context.Context, _ int64, credentials map[string]any) error {
- r.mu.Lock()
- defer r.mu.Unlock()
- r.credentials = credentials
- return nil
- }
- func mustAgentIdentityJSON(t *testing.T, value any) []byte {
- t.Helper()
- encoded, err := json.Marshal(value)
- require.NoError(t, err)
- return encoded
- }
|