@@ -26,6 +26,12 @@ var gatewaySkillFallback string
2626//go:embed hook_gateway_detect.sh
2727var gatewayDetectHook string
2828
29+ //go:embed plugin_gateway_hermes.yaml
30+ var hermesPluginManifest string
31+
32+ //go:embed plugin_gateway_hermes.py
33+ var hermesPluginHandler string
34+
2935// RunCmd is `onecli run -- <command> [args...]`.
3036type RunCmd struct {
3137 Project string `optional:"" short:"p" help:"Project slug."`
@@ -113,15 +119,18 @@ func (c *RunCmd) Run(out *output.Writer) error {
113119 // For known agents, fetch the agent-specific skill variant and install
114120 // to the agent's skill directory. Also optionally register a hook.
115121 agentFramework := strings .ToLower (filepath .Base (c .Args [0 ]))
116- if name , dir , cfgDir , noHook , _ , nativeProxy , ok := agentSkillDir (c .Args [0 ]); ok {
122+ if name , dir , cfgDir , noHook , plugin , nativeProxy , ok := agentSkillDir (c .Args [0 ]); ok {
117123 skillContent := gatewaySkillFallback
118- if fetched , err := client .GetGatewaySkill (newContext ()); err == nil && fetched != "" {
124+ if fetched , err := client .GetGatewaySkill (newContext (), agentFramework ); err == nil && fetched != "" {
119125 skillContent = fetched
120126 }
121127 maybeInstallGatewaySkill (out , name , dir , skillContent )
122128 if ! noHook {
123129 maybeInstallGatewayHook (out , name , dir )
124130 }
131+ if plugin {
132+ maybeInstallGatewayPlugin (out , name , dir )
133+ }
125134
126135 // Electron-based agents (e.g. Cursor) ignore embedded user:pass in
127136 // HTTPS_PROXY and show a native auth dialog. Inject proxy credentials
@@ -142,7 +151,7 @@ func (c *RunCmd) Run(out *output.Writer) error {
142151 // Unknown agent — install the skill to ~/.onecli/skills/ so the
143152 // framework can discover it via ONECLI_GATEWAY_SKILL_PATH.
144153 skillContent := gatewaySkillFallback
145- if fetched , err := client .GetGatewaySkill (newContext ()); err == nil && fetched != "" {
154+ if fetched , err := client .GetGatewaySkill (newContext (), agentFramework ); err == nil && fetched != "" {
146155 skillContent = fetched
147156 }
148157 if p := installUniversalGatewaySkill (out , skillContent ); p != "" {
@@ -502,6 +511,152 @@ func maybeInjectNativeProxyConfig(out *output.Writer, agentName, configRelDir st
502511 }
503512}
504513
514+ // maybeInstallGatewayPlugin installs a transform_tool_result plugin that
515+ // intercepts auth errors and appends gateway recovery instructions.
516+ // It also enables the plugin in the agent's config.yaml.
517+ func maybeInstallGatewayPlugin (out * output.Writer , agentName , baseDir string ) {
518+ home , err := os .UserHomeDir ()
519+ if err != nil {
520+ return
521+ }
522+ pluginDir := filepath .Join (home , baseDir , "plugins" , "onecli-gateway" )
523+
524+ // Write plugin.yaml.
525+ manifestPath := filepath .Join (pluginDir , "plugin.yaml" )
526+ existingManifest , _ := os .ReadFile (manifestPath )
527+ if ! bytes .Equal (existingManifest , []byte (hermesPluginManifest )) {
528+ if err := os .MkdirAll (pluginDir , 0o750 ); err != nil {
529+ out .Stderr (fmt .Sprintf ("onecli: warning: could not create plugin directory: %v" , err ))
530+ return
531+ }
532+ if err := os .WriteFile (manifestPath , []byte (hermesPluginManifest ), 0o600 ); err != nil {
533+ out .Stderr (fmt .Sprintf ("onecli: warning: could not write plugin manifest: %v" , err ))
534+ return
535+ }
536+ }
537+
538+ // Write __init__.py.
539+ handlerPath := filepath .Join (pluginDir , "__init__.py" )
540+ existingHandler , _ := os .ReadFile (handlerPath )
541+ if ! bytes .Equal (existingHandler , []byte (hermesPluginHandler )) {
542+ if err := os .WriteFile (handlerPath , []byte (hermesPluginHandler ), 0o600 ); err != nil {
543+ out .Stderr (fmt .Sprintf ("onecli: warning: could not write plugin handler: %v" , err ))
544+ return
545+ }
546+ out .Stderr (fmt .Sprintf ("onecli: installed gateway plugin for %s." , agentName ))
547+ }
548+
549+ // Enable the plugin in config.yaml if not already listed.
550+ configPath := filepath .Join (home , baseDir , "config.yaml" )
551+ configData , _ := os .ReadFile (configPath )
552+ configStr := string (configData )
553+
554+ if ! strings .Contains (configStr , "onecli-gateway" ) {
555+ // Append a plugins.enabled entry. If the file has no plugins section,
556+ // add one. If it does, append to the enabled list.
557+ if strings .Contains (configStr , "plugins:" ) {
558+ if strings .Contains (configStr , "enabled:" ) {
559+ // Add to existing enabled list — insert after "enabled:" line.
560+ configStr = strings .Replace (configStr , "enabled:" , "enabled:\n - onecli-gateway" , 1 )
561+ } else {
562+ configStr = strings .Replace (configStr , "plugins:" , "plugins:\n enabled:\n - onecli-gateway" , 1 )
563+ }
564+ } else {
565+ if configStr != "" && ! strings .HasSuffix (configStr , "\n " ) {
566+ configStr += "\n "
567+ }
568+ configStr += "\n plugins:\n enabled:\n - onecli-gateway\n "
569+ }
570+ if err := os .WriteFile (configPath , []byte (configStr ), 0o600 ); err != nil {
571+ out .Stderr (fmt .Sprintf ("onecli: warning: could not enable plugin in config.yaml: %v" , err ))
572+ return
573+ }
574+ out .Stderr (fmt .Sprintf ("onecli: enabled gateway plugin in %s config." , agentName ))
575+ }
576+
577+ // Ensure proxy URL env vars are forwarded into Docker containers.
578+ // CA cert file-path vars are NOT forwarded — host paths don't exist
579+ // in the container. Instead we mount the CA bundle and set docker_env.
580+ proxyEnvs := []string {"HTTPS_PROXY" , "HTTP_PROXY" , "https_proxy" , "http_proxy" , "NO_PROXY" , "no_proxy" , "ONECLI_GATEWAY" }
581+ configData , _ = os .ReadFile (configPath )
582+ configStr = string (configData )
583+ changed := false
584+ for _ , env := range proxyEnvs {
585+ if ! strings .Contains (configStr , env ) {
586+ configStr = strings .Replace (configStr , "docker_forward_env: []" , "docker_forward_env:\n - " + env , 1 )
587+ if ! strings .Contains (configStr , env ) {
588+ configStr = strings .Replace (configStr , "docker_forward_env:" , "docker_forward_env:\n - " + env , 1 )
589+ }
590+ changed = true
591+ }
592+ }
593+
594+ // Mount the CA bundle into the container and set CA env vars to the
595+ // container-side path so TLS works through the gateway proxy.
596+ caPath := filepath .Join (home , ".onecli" , "ca-bundle.pem" )
597+ const containerCAPath = "/etc/ssl/certs/onecli-ca-bundle.pem"
598+ volumeEntry := caPath + ":" + containerCAPath + ":ro"
599+ if ! strings .Contains (configStr , "onecli-ca-bundle" ) {
600+ configStr = strings .Replace (configStr , "docker_volumes: []" , "docker_volumes:\n - " + volumeEntry , 1 )
601+ if ! strings .Contains (configStr , "onecli-ca-bundle" ) {
602+ configStr = strings .Replace (configStr , "docker_volumes:" , "docker_volumes:\n - " + volumeEntry , 1 )
603+ }
604+ changed = true
605+ }
606+
607+ // Set CA env vars and proxy URLs inside Docker via docker_env.
608+ // docker_forward_env doesn't reliably forward to persistent containers,
609+ // so we inject the actual values directly.
610+ dockerEnvs := map [string ]string {
611+ "SSL_CERT_FILE" : containerCAPath ,
612+ "NODE_EXTRA_CA_CERTS" : containerCAPath ,
613+ "REQUESTS_CA_BUNDLE" : containerCAPath ,
614+ "CURL_CA_BUNDLE" : containerCAPath ,
615+ "ONECLI_GATEWAY" : "true" ,
616+ }
617+ for _ , k := range []string {"HTTPS_PROXY" , "HTTP_PROXY" , "https_proxy" , "http_proxy" } {
618+ if v := os .Getenv (k ); v != "" {
619+ dockerEnvs [k ] = v
620+ }
621+ }
622+ caEnvs := dockerEnvs
623+ for k , v := range caEnvs {
624+ entry := k + ": " + v
625+ if ! strings .Contains (configStr , entry ) {
626+ configStr = strings .Replace (configStr , "docker_env: {}" , "docker_env:\n " + entry , 1 )
627+ if ! strings .Contains (configStr , entry ) {
628+ configStr = strings .Replace (configStr , "docker_env:" , "docker_env:\n " + entry , 1 )
629+ }
630+ changed = true
631+ }
632+ }
633+
634+ if changed {
635+ if err := os .WriteFile (configPath , []byte (configStr ), 0o600 ); err != nil {
636+ out .Stderr (fmt .Sprintf ("onecli: warning: could not update Docker config: %v" , err ))
637+ } else {
638+ out .Stderr (fmt .Sprintf ("onecli: configured Docker env and CA cert for %s." , agentName ))
639+ removeStaleAgentContainers (out , agentName )
640+ }
641+ }
642+ }
643+
644+ // removeStaleAgentContainers removes persistent Docker containers that were
645+ // created before the config changed. The agent will recreate them with the
646+ // updated env vars and volume mounts on the next tool execution.
647+ func removeStaleAgentContainers (out * output.Writer , agentName string ) {
648+ prefix := strings .ToLower (strings .ReplaceAll (agentName , " " , "-" ))
649+ ids , err := exec .Command ("docker" , "ps" , "-aq" , "--filter" , "name=" + prefix ).Output ()
650+ if err != nil || len (bytes .TrimSpace (ids )) == 0 {
651+ return
652+ }
653+ for _ , id := range strings .Fields (strings .TrimSpace (string (ids ))) {
654+ if rmErr := exec .Command ("docker" , "rm" , "-f" , id ).Run (); rmErr == nil {
655+ out .Stderr (fmt .Sprintf ("onecli: removed stale %s container %s." , agentName , id [:12 ]))
656+ }
657+ }
658+ }
659+
505660// maybeInstallGatewayHook installs the gateway detection hook script and
506661// registers it in the agent's settings.json so the agent knows the gateway
507662// is active without needing to run any visible checks.
0 commit comments