@@ -14,6 +14,7 @@ import (
1414 "strings"
1515 "syscall"
1616
17+ "github.com/onecli/onecli-cli/internal/api"
1718 "github.com/onecli/onecli-cli/internal/config"
1819 "github.com/onecli/onecli-cli/pkg/output"
1920 "github.com/onecli/onecli-cli/pkg/validate"
@@ -107,22 +108,46 @@ func (c *RunCmd) Run(out *output.Writer) error {
107108 // Build child environment.
108109 env := buildChildEnv (os .Environ (), cfg .Env , caPath )
109110
110- // Install skill and hook for known agents (silently updates stale files).
111- // Fetch the latest skill from the API; fall back to the embedded copy.
112- if name , dir , cfgDir , ok := agentSkillDir (c .Args [0 ]); ok {
111+ env = append (env , "ONECLI_GATEWAY=true" )
112+
113+ // For known agents, fetch the agent-specific skill variant and install
114+ // to the agent's skill directory. Also optionally register a hook.
115+ agentFramework := strings .ToLower (filepath .Base (c .Args [0 ]))
116+ if name , dir , cfgDir , noHook , _ , nativeProxy , ok := agentSkillDir (c .Args [0 ]); ok {
113117 skillContent := gatewaySkillFallback
114118 if fetched , err := client .GetGatewaySkill (newContext ()); err == nil && fetched != "" {
115119 skillContent = fetched
116120 }
117121 maybeInstallGatewaySkill (out , name , dir , skillContent )
118- maybeInstallGatewayHook (out , name , dir )
122+ if ! noHook {
123+ maybeInstallGatewayHook (out , name , dir )
124+ }
119125
120126 // Electron-based agents (e.g. Cursor) ignore embedded user:pass in
121127 // HTTPS_PROXY and show a native auth dialog. Inject proxy credentials
122128 // into the app's VS Code-style settings.json instead.
123129 if cfgDir != "" {
124130 env = injectElectronProxySettings (out , env , cfgDir , caPath )
125131 }
132+
133+ // Agents with a native proxy config (e.g. Codex) need proxy_url
134+ // written to their TOML config and CODEX_CA_CERTIFICATE set.
135+ if nativeProxy != "" {
136+ maybeInjectNativeProxyConfig (out , name , nativeProxy , env , caPath )
137+ }
138+ if agentFramework == "codex" {
139+ maybeCreateCodexAuthStub (out , client )
140+ }
141+ } else {
142+ // Unknown agent — install the skill to ~/.onecli/skills/ so the
143+ // framework can discover it via ONECLI_GATEWAY_SKILL_PATH.
144+ skillContent := gatewaySkillFallback
145+ if fetched , err := client .GetGatewaySkill (newContext ()); err == nil && fetched != "" {
146+ skillContent = fetched
147+ }
148+ if p := installUniversalGatewaySkill (out , skillContent ); p != "" {
149+ env = append (env , "ONECLI_GATEWAY_SKILL_PATH=" + p )
150+ }
126151 }
127152
128153 // Surface any warnings from the server (e.g. missing credentials).
@@ -314,30 +339,33 @@ func rewriteProxyEnvHosts(env map[string]string, localHost string) {
314339
315340// supportedAgents maps CLI binary base-names to (agentName, skillsBaseDir) pairs.
316341var supportedAgents = []struct {
317- bases []string
318- agentName string
319- baseDir string
320- configDir string // VS Code-style config dir name; non-empty enables proxy settings injection.
342+ bases []string
343+ agentName string
344+ baseDir string
345+ configDir string // VS Code-style config dir name; non-empty enables proxy settings injection.
346+ skipHook bool // true for agents that don't support Claude Code-style hooks.
347+ hasPlugin bool // true for agents that support a transform_tool_result plugin.
348+ nativeProxyConfig string // home-relative dir containing a TOML config that needs proxy_url injection (e.g. ".codex").
321349}{
322- {[]string {"claude" }, "Claude Code" , ".claude" , "" },
323- {[]string {"cursor" , "agent" }, "Cursor" , ".cursor" , "Cursor" },
324- {[]string {"codex" }, "Codex" , ".agents" , "" },
325- {[]string {"hermes" }, "Hermes" , ".hermes" , "" },
326- {[]string {"opencode" }, "OpenCode" , ".opencode" , "" },
350+ {[]string {"claude" }, "Claude Code" , ".claude" , "" , false , false , "" },
351+ {[]string {"cursor" , "agent" }, "Cursor" , ".cursor" , "Cursor" , false , false , "" },
352+ {[]string {"codex" }, "Codex" , ".agents" , "" , false , false , ".codex" },
353+ {[]string {"hermes" }, "Hermes" , ".hermes" , "" , true , true , "" },
354+ {[]string {"opencode" }, "OpenCode" , ".opencode" , "" , false , false , "" },
327355}
328356
329- // agentSkillDir returns the display name and skills base directory for a known
330- // agent command, or ok=false if the command is not recognized.
331- func agentSkillDir (cmd string ) (agentName , baseDir , configDir string , ok bool ) {
357+ // agentSkillDir returns the display name, skills base directory, and config
358+ // options for a known agent command, or ok=false if the command is not recognized.
359+ func agentSkillDir (cmd string ) (agentName , baseDir , configDir string , skipHook bool , hasPlugin bool , nativeProxyConfig string , ok bool ) {
332360 base := filepath .Base (cmd )
333361 for _ , a := range supportedAgents {
334362 for _ , b := range a .bases {
335363 if base == b {
336- return a .agentName , a .baseDir , a .configDir , true
364+ return a .agentName , a .baseDir , a .configDir , a . skipHook , a . hasPlugin , a . nativeProxyConfig , true
337365 }
338366 }
339367 }
340- return "" , "" , "" , false
368+ return "" , "" , "" , false , false , "" , false
341369}
342370
343371// maybeInstallGatewaySkill installs the OneCLI gateway skill file if it is
@@ -366,6 +394,114 @@ func maybeInstallGatewaySkill(out *output.Writer, agentName, baseDir, content st
366394 out .Stderr (fmt .Sprintf ("onecli: installed gateway skill for %s." , agentName ))
367395}
368396
397+ // installUniversalGatewaySkill writes the gateway skill to
398+ // ~/.onecli/skills/gateway.md so any framework can reference it via
399+ // the ONECLI_GATEWAY_SKILL_PATH env var. Returns the path on success.
400+ func installUniversalGatewaySkill (out * output.Writer , content string ) string {
401+ home , err := os .UserHomeDir ()
402+ if err != nil {
403+ return ""
404+ }
405+ fullPath := filepath .Join (home , ".onecli" , "skills" , "gateway.md" )
406+
407+ existing , err := os .ReadFile (fullPath )
408+ if err == nil && bytes .Equal (existing , []byte (content )) {
409+ return fullPath
410+ }
411+
412+ if err := os .MkdirAll (filepath .Dir (fullPath ), 0o750 ); err != nil {
413+ out .Stderr (fmt .Sprintf ("onecli: warning: could not create universal skill directory: %v" , err ))
414+ return ""
415+ }
416+ if err := os .WriteFile (fullPath , []byte (content ), 0o600 ); err != nil {
417+ out .Stderr (fmt .Sprintf ("onecli: warning: could not write universal skill file: %v" , err ))
418+ return ""
419+ }
420+ return fullPath
421+ }
422+
423+ // codexAuthStub is the auth.json stub written to ~/.codex/auth.json when the
424+ // file does not exist. The id_token is a structurally valid JWT with email and
425+ // plan_type claims so Codex's local validation passes. Real credentials are
426+ // injected at the gateway proxy level.
427+ const codexAuthStub = `{
428+ "auth_mode": "chatgpt",
429+ "OPENAI_API_KEY": null,
430+ "tokens": {
431+ "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvbmVjbGktbWFuYWdlZCIsImVtYWlsIjoib25lY2xpQG9uZWNsaS5zaCIsImV4cCI6NDEwMjQ0NDgwMCwiaWF0IjoxNzM1Njg5NjAwLCJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9wbGFuX3R5cGUiOiJmcmVlIiwiY2hhdGdwdF91c2VyX2lkIjoib25lY2xpLW1hbmFnZWQiLCJjaGF0Z3B0X2FjY291bnRfaWQiOiJvbmVjbGktbWFuYWdlZCJ9fQ.b25lY2xpLW1hbmFnZWQtc2lnbmF0dXJl",
432+ "access_token": "onecli-managed",
433+ "refresh_token": "onecli-managed",
434+ "account_id": "onecli-managed"
435+ },
436+ "last_refresh": "2025-01-01T00:00:00Z"
437+ }
438+ `
439+
440+ // maybeCreateCodexAuthStub creates ~/.codex/auth.json with onecli-managed
441+ // placeholder values if the file does not already exist. Fetches the latest
442+ // stub from the API; falls back to the embedded constant.
443+ func maybeCreateCodexAuthStub (out * output.Writer , client * api.Client ) {
444+ home , err := os .UserHomeDir ()
445+ if err != nil {
446+ return
447+ }
448+ authPath := filepath .Join (home , ".codex" , "auth.json" )
449+ if _ , err := os .Stat (authPath ); err == nil {
450+ return
451+ }
452+
453+ content := codexAuthStub
454+ if stub , err := client .GetCredentialStub (newContext (), "codex" ); err == nil && stub .Content != "" {
455+ content = stub .Content
456+ }
457+
458+ if err := os .MkdirAll (filepath .Dir (authPath ), 0o750 ); err != nil {
459+ out .Stderr (fmt .Sprintf ("onecli: warning: could not create .codex directory: %v" , err ))
460+ return
461+ }
462+ if err := os .WriteFile (authPath , []byte (content ), 0o600 ); err != nil {
463+ out .Stderr (fmt .Sprintf ("onecli: warning: could not write codex auth stub: %v" , err ))
464+ return
465+ }
466+ out .Stderr ("onecli: created ~/.codex/auth.json stub for gateway auth." )
467+ }
468+
469+ // maybeInjectNativeProxyConfig writes proxy_url into a TOML config file for
470+ // agents that have their own managed proxy (e.g. Codex). Also sets the
471+ // agent-specific CA certificate env var.
472+ func maybeInjectNativeProxyConfig (out * output.Writer , agentName , configRelDir string , env []string , caPath string ) {
473+ home , err := os .UserHomeDir ()
474+ if err != nil {
475+ return
476+ }
477+
478+ proxyURL := findProxyURL (env )
479+ if proxyURL == "" {
480+ return
481+ }
482+
483+ configPath := filepath .Join (home , configRelDir , "config.toml" )
484+ data , _ := os .ReadFile (configPath )
485+ content := string (data )
486+
487+ // Inject [network] section with proxy_url if not already present.
488+ if ! strings .Contains (content , "proxy_url" ) {
489+ section := "\n [network]\n proxy_url = \" " + proxyURL + "\" \n "
490+ content += section
491+ if err := os .WriteFile (configPath , []byte (content ), 0o600 ); err != nil {
492+ out .Stderr (fmt .Sprintf ("onecli: warning: could not write proxy config for %s: %v" , agentName , err ))
493+ return
494+ }
495+ out .Stderr (fmt .Sprintf ("onecli: configured native proxy for %s." , agentName ))
496+ }
497+
498+ // Set CODEX_CA_CERTIFICATE if we have a CA path — Codex reads this
499+ // in addition to SSL_CERT_FILE for its Rust TLS client.
500+ if caPath != "" {
501+ os .Setenv ("CODEX_CA_CERTIFICATE" , caPath )
502+ }
503+ }
504+
369505// maybeInstallGatewayHook installs the gateway detection hook script and
370506// registers it in the agent's settings.json so the agent knows the gateway
371507// is active without needing to run any visible checks.
0 commit comments