Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,35 @@ jobs:
- name: Configure Git for private modules
run: git config --global url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"

- name: Provision cluster
uses: agynio/bootstrap/.github/actions/provision@main
- name: Checkout agn-cli
uses: actions/checkout@v4
with:
repository: agynio/agn-cli
token: ${{ secrets.GITHUB_TOKEN }}
path: .agn-cli

- name: Setup DevSpace
uses: agynio/e2e/.github/actions/setup-devspace@main
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.x'

- name: Run E2E tests
uses: agynio/e2e/.github/actions/run-tests@main
- name: Install buf
uses: bufbuild/buf-setup-action@v1
with:
service: agynd_cli
github_token: ${{ secrets.GITHUB_TOKEN }}
version: 1.66.0

- name: Generate protobuf bindings
run: |
buf generate buf.build/agynio/api \
--path agynio/api/gateway/v1 \
--include-imports

- name: Install Codex CLI
run: npm install -g @openai/codex

- name: Run repo-local E2E tests
run: go test -v -count=1 -tags e2e ./test/e2e/
Comment thread
noa-lucent marked this conversation as resolved.
env:
OPENAI_API_KEY: test-key
AGN_REPO_PATH: ${{ github.workspace }}/.agn-cli
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@ Full setup: https://github.com/agynio/architecture/blob/main/architecture/operat
devspace dev
devspace dev -w
```

## E2E validation

The GitHub E2E workflow runs this repository's local E2E tests with:

```bash
go test -v -count=1 -tags e2e ./test/e2e/
```

Those tests validate the local agent CLI bridge behavior against deterministic
TestLLM endpoints through the Codex and AGN SDK flows. The workflow checks out
`agynio/agn-cli` so the AGN coverage builds and runs the current AGN CLI during
the test. They also build and execute this repository's `cmd/agynd` binary with
`/agyn-bin/config.json` installed by the test harness. That test uses a stub
Gateway and fake AGN agent to verify daemon startup, platform initialization,
and subscriber startup without requiring a full cluster.

This repository does not run the centralized `agynio/e2e` smoke suite. Broader
platform and service smoke coverage remains owned by the centralized E2E
repository and service-specific workflows.
3 changes: 2 additions & 1 deletion internal/daemon/agn.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func newAgnDaemon(ctx context.Context, cfg config.Config, version string) (*Daem

tracingProxy, err := tracingproxy.Start(ctx, tracingproxy.Config{
TracingAddress: cfg.TracingAddress,
ListenAddress: tracingProxyListenAddress,
ThreadID: cfg.ThreadID,
WorkloadID: cfg.WorkloadID,
})
Expand All @@ -67,7 +68,7 @@ func newAgnDaemon(ctx context.Context, cfg config.Config, version string) (*Daem
return nil, err
}

otlpEndpoint := "http://" + tracingproxy.ListenAddress
otlpEndpoint := "http://" + tracingProxy.Address()
agnClient, err := agnsdk.Start(ctx, agnsdk.Options{
BinaryPath: cfg.AgentBinary,
Env: []string{
Expand Down
3 changes: 2 additions & 1 deletion internal/daemon/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func newClaudeDaemon(ctx context.Context, cfg config.Config, version string) (*D

tracingProxy, err := tracingproxy.Start(ctx, tracingproxy.Config{
TracingAddress: cfg.TracingAddress,
ListenAddress: tracingProxyListenAddress,
ThreadID: cfg.ThreadID,
WorkloadID: cfg.WorkloadID,
})
Expand All @@ -47,7 +48,7 @@ func newClaudeDaemon(ctx context.Context, cfg config.Config, version string) (*D
return nil, err
}

otlpEndpoint := "http://" + tracingproxy.ListenAddress
otlpEndpoint := "http://" + tracingProxy.Address()
options := claude.Options{
BinaryPath: cfg.AgentBinary,
WorkDir: cfg.WorkDir,
Expand Down
8 changes: 3 additions & 5 deletions internal/daemon/codexconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"sync"

"github.com/agynio/agynd-cli/internal/config"
"github.com/agynio/agynd-cli/internal/tracingproxy"
codex "github.com/agynio/codex-sdk-go"
)

Expand Down Expand Up @@ -52,21 +51,20 @@ var codexAuthEnvVars = []string{
codexEnvCodexAccessToken,
}

func writeCodexConfig(llmBaseURL string, mcpServers []config.MCPServer) (string, error) {
func writeCodexConfig(llmBaseURL string, mcpServers []config.MCPServer, otlpEndpoint string) (string, error) {
codexHome := filepath.Join(codexHomeEnv(), ".codex")
if err := os.MkdirAll(codexHome, 0o700); err != nil {
return "", fmt.Errorf("create codex home dir: %w", err)
}
configPath := filepath.Join(codexHome, "config.toml")
payload := codexConfig(llmBaseURL, mcpServers)
payload := codexConfig(llmBaseURL, mcpServers, otlpEndpoint)
if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil {
return "", fmt.Errorf("write codex config: %w", err)
}
return codexHome, nil
}

func codexConfig(llmBaseURL string, mcpServers []config.MCPServer) string {
otlpEndpoint := "http://" + tracingproxy.ListenAddress
func codexConfig(llmBaseURL string, mcpServers []config.MCPServer, otlpEndpoint string) string {
apiKeyEnv := codexAPIKeyEnv
if isZitiLLMBaseURL(llmBaseURL) {
apiKeyEnv = ""
Expand Down
28 changes: 14 additions & 14 deletions internal/daemon/codexconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"testing"

"github.com/agynio/agynd-cli/internal/config"
"github.com/agynio/agynd-cli/internal/tracingproxy"
codex "github.com/agynio/codex-sdk-go"
)

Expand All @@ -31,12 +30,14 @@ func (noopCodexClient) Close() error {
return nil
}

const testCodexOTLPEndpoint = "http://127.0.0.1:54321"

func TestWriteCodexConfig(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)

baseURL := "https://example.com"
codexHome, err := writeCodexConfig(baseURL, nil)
codexHome, err := writeCodexConfig(baseURL, nil, testCodexOTLPEndpoint)
if err != nil {
t.Fatalf("expected config to be written, got %v", err)
}
Expand All @@ -62,7 +63,7 @@ func TestWriteCodexConfigHomeFallback(t *testing.T) {
t.Setenv("HOME", "")

baseURL := "https://example.com"
codexHome, err := writeCodexConfig(baseURL, nil)
codexHome, err := writeCodexConfig(baseURL, nil, testCodexOTLPEndpoint)
if err != nil {
t.Fatalf("expected config to be written, got %v", err)
}
Expand All @@ -89,7 +90,7 @@ func TestWriteCodexConfigForZitiOmitsAPIKeyEnv(t *testing.T) {
t.Setenv("HOME", tmpHome)

baseURL := "http://llm-proxy.ziti:443/v1"
codexHome, err := writeCodexConfig(baseURL, nil)
codexHome, err := writeCodexConfig(baseURL, nil, testCodexOTLPEndpoint)
if err != nil {
t.Fatalf("expected config to be written, got %v", err)
}
Expand All @@ -115,7 +116,7 @@ func TestWriteCodexConfigWithMCPServers(t *testing.T) {
{Name: "memory", Port: 8100},
{Name: "cache", Port: 8200},
}
codexHome, err := writeCodexConfig(baseURL, mcpServers)
codexHome, err := writeCodexConfig(baseURL, mcpServers, testCodexOTLPEndpoint)
if err != nil {
t.Fatalf("expected config to be written, got %v", err)
}
Expand Down Expand Up @@ -152,7 +153,7 @@ func TestCodexEnvIncludesAPIKeyForPublicLLM(t *testing.T) {
LLMAPIToken: "token-123",
}

env := codexEnv(cfg, "/tmp/.codex", "/tmp", "http://127.0.0.1:4317")
env := codexEnv(cfg, "/tmp/.codex", "/tmp", testCodexOTLPEndpoint)

if env[codexEnvOpenAIAPIKey] != "token-123" {
t.Fatalf("expected API key in codex env, got %q", env[codexEnvOpenAIAPIKey])
Expand All @@ -167,7 +168,7 @@ func TestCodexEnvOmitsAPIKeyForZitiLLM(t *testing.T) {
LLMAPIToken: "user-token",
}

env := codexEnv(cfg, "/tmp/.codex", "/tmp", "http://127.0.0.1:4317")
env := codexEnv(cfg, "/tmp/.codex", "/tmp", testCodexOTLPEndpoint)

if _, ok := env[codexEnvOpenAIAPIKey]; ok {
t.Fatalf("expected ziti codex env to omit %s", codexEnvOpenAIAPIKey)
Expand Down Expand Up @@ -217,8 +218,8 @@ func TestZitiCodexProcessReceivesNoAuthEnvConfig(t *testing.T) {
LLMAPIToken: "agent-env-token",
}

env := codexEnv(cfg, "/tmp/.codex", "/tmp", "http://127.0.0.1:4317")
configPayload := codexConfig(cfg.LLMBaseURL, nil)
env := codexEnv(cfg, "/tmp/.codex", "/tmp", testCodexOTLPEndpoint)
configPayload := codexConfig(cfg.LLMBaseURL, nil, testCodexOTLPEndpoint)
seenEnv := map[string]bool{}
_, err := withoutCodexAuthEnv(func() (codexClient, error) {
for _, key := range codexAuthEnvVars {
Expand Down Expand Up @@ -277,22 +278,21 @@ func assertCodexBaseEnv(t *testing.T, env map[string]string) {
if env[codexEnvHome] != "/tmp" {
t.Fatalf("expected HOME, got %q", env[codexEnvHome])
}
if env[codexEnvOTELExporterOTLPEndpoint] != "http://127.0.0.1:4317" {
if env[codexEnvOTELExporterOTLPEndpoint] != testCodexOTLPEndpoint {
t.Fatalf("expected OTLP endpoint, got %q", env[codexEnvOTELExporterOTLPEndpoint])
}
}

func codexConfigWithAPIKey(baseURL string) string {
return codexConfigPayload(baseURL, `env_key = "OPENAI_API_KEY"
return codexConfigPayload(baseURL, testCodexOTLPEndpoint, `env_key = "OPENAI_API_KEY"
`)
}

func codexConfigWithoutAPIKey(baseURL string) string {
return codexConfigPayload(baseURL, "")
return codexConfigPayload(baseURL, testCodexOTLPEndpoint, "")
}

func codexConfigPayload(baseURL, apiKeyEnv string) string {
otlpEndpoint := "http://" + tracingproxy.ListenAddress
func codexConfigPayload(baseURL, otlpEndpoint, apiKeyEnv string) string {
return fmt.Sprintf(`model_provider = "platform"
approval_policy = "never"
sandbox_mode = "danger-full-access"
Expand Down
28 changes: 16 additions & 12 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
opAgnTurn = "agn_turn"
opClaudeTurn = "claude_turn"
opProcessSignalShutdown = "process_signal/shutdown"
tracingProxyListenAddress = "127.0.0.1:0"
)

const (
Expand Down Expand Up @@ -253,29 +254,32 @@ func newCodexDaemon(ctx context.Context, cfg config.Config, version string) (*Da
tracker := codexbridge.NewTurnTracker()
bridge := codexbridge.New(tracker)
threadsMapping := codexbridge.NewThreadMapping()
codexHome, err := writeCodexConfig(cfg.LLMBaseURL, cfg.MCPServers)

tracingProxy, err := tracingproxy.Start(ctx, tracingproxy.Config{
TracingAddress: cfg.TracingAddress,
ListenAddress: tracingProxyListenAddress,
ThreadID: cfg.ThreadID,
WorkloadID: cfg.WorkloadID,
})
if err != nil {
_ = setup.gatewayConn.Close()
return nil, err
}

if err := runInitScripts(ctx, setup.agents, cfg.AgentID.String(), cfg.WorkDir); err != nil {
otlpEndpoint := "http://" + tracingProxy.Address()
Comment thread
noa-lucent marked this conversation as resolved.
codexHome, err := writeCodexConfig(cfg.LLMBaseURL, cfg.MCPServers, otlpEndpoint)
if err != nil {
tracingProxy.Close()
_ = setup.gatewayConn.Close()
return nil, err
}
codexHomeValue := codexHomeEnv()
mappingStore := codexbridge.NewThreadMappingStore(codexHomeValue)

tracingProxy, err := tracingproxy.Start(ctx, tracingproxy.Config{
TracingAddress: cfg.TracingAddress,
ThreadID: cfg.ThreadID,
WorkloadID: cfg.WorkloadID,
})
if err != nil {
if err := runInitScripts(ctx, setup.agents, cfg.AgentID.String(), cfg.WorkDir); err != nil {
tracingProxy.Close()
_ = setup.gatewayConn.Close()
return nil, err
}
otlpEndpoint := "http://" + tracingproxy.ListenAddress
codexHomeValue := codexHomeEnv()
mappingStore := codexbridge.NewThreadMappingStore(codexHomeValue)
options := []codex.Option{
codex.WithBinary(cfg.AgentBinary),
codex.WithWorkDir(cfg.WorkDir),
Expand Down
Loading
Loading