1

I am trying to write a small Go wrapper application around the 1Password CLI executable op like so:

package main

import (
    "fmt"
    "log"
    "os/exec"
)

func main() {
    op := exec.Command("op", "item", "list")
    out, err := op.Output()
    if e, ok := err.(*exec.ExitError); ok {
        log.Fatal(e, ": ", string(e.Stderr))
    }
    fmt.Println(string(out))
}

However, I keep getting the following error:

2023/04/13 09:48:51 exit status 1: [ERROR] 2023/04/13 09:48:51 error initializing client: connecting to desktop app: read: connection reset, make sure the CLI is correctly installed and Connect with 1Password CLI is enabled in the 1Password app

But when I do the same thing from a Python script like so:

#!/usr/bin/env python

import subprocess

subprocess.run(["op", "item", "list"])

...I get the output just fine.

Interestingly enough though, when I call the Python script (named op.py) from the Go app, it works fine (modified Go app shown below):

package main

import (
    "fmt"
    "log"
    "os/exec"
)

func main() {
    op := exec.Command("/usr/bin/python3.11", "./op.py")
    //op := exec.Command("op", "item", "list")
    out, err := op.Output()
    if e, ok := err.(*exec.ExitError); ok {
        log.Fatal(e, ": ", string(e.Stderr))
    }
    fmt.Println(string(out))
}

I can test that it is being printed by the Go app and not by the Python script because if I remove the fmt.Printf(...), nothing gets printed.

So to summarize:

  • Go -> op: does not work
  • Python (./op.py) -> op: works fine
  • Go -> Python (./op.py) -> op: works fine
wheeler
  • 2,823
  • 3
  • 27
  • 43
  • I hypothesize that in the first case the program actually runs, prints that _warning_ to its stderr and maybe even exits with a non-zero code, but does also print that JSON to its stdout—you just never get to examine it. In the second case all seems to just work because you do no pass `check=True` to `subprocess.run` so it does not raise an exception even is the process misbehaved. Can you check this? I'd start with the shell: if you merely run the command in your shell, does it print that warning to stderr? Does it exit with a non-zero exit code (do `echo $?` right after the process exits)? – kostix Apr 13 '23 at 15:36
  • Adding `check=True` to the `subprocess.run(...)` call has no effect as it is exiting cleanly when run through Python. When I run the command directly through my shell (bash in my case), there is nothing printed to stderr. – wheeler Apr 13 '23 at 15:40
  • Very interesting. What if you do `op := exec.Command("/bin/sh", "op item get DocSend --format=json")` instead? I mean, make the shell run the command, as opposed to running it directly. – kostix Apr 13 '23 at 16:05
  • I assume you meant to add `"-c"` after the `"/bin/sh"`, and so I get the same error when doing this: `op := exec.Command("/bin/sh", "-c", "op item get DocSend --format=json")`. I also updated my question to report an interesting finding: I can call the python script from the go app and it works fine. – wheeler Apr 13 '23 at 16:12
  • 1
    Are you running them from the same shell session? Can it be that the environment for a go application is different from python one? – Vüsal Apr 13 '23 at 16:28
  • @Vüsal Yes. They are being run from the same terminal window. – wheeler Apr 18 '23 at 19:16
  • alright. Just to be clear, they're being ran from the same terminal window and tab and split - same shell session with the same environment using the same shell user. If that's the case - I'm completely puzzled. – Vüsal Apr 19 '23 at 09:10
  • Is the Python script being run using a Python interpreter directly (you run it as `python[3] script.py` or you run it as `./script.py` _and_ the script has its shebang line set to a python interpreter–like in `#! /usr/bin/python3`) or you're running it via the `env` wrapper–either by hand or the script's shebang line reads something like `#! /usr/bin/env python3`)? @Vüsal, what do you think, can something like this explain the observed differences? – kostix Apr 19 '23 at 12:01
  • @kostix I think OP is explicitly referring to python interpreter. `op := exec.Command("/usr/bin/python3.11", "./op.py")` - regarding your question - I think in both cases the python code is run on behalf of a user who runs it. The `env ` is just about finding a python interpreter from the $PATH, AFAIK, not a python expert though. – Vüsal Apr 19 '23 at 14:31
  • Running it every different way with Python works fine—calling the `python` executable, calling the wrapper script with the shebang just `python`, calling the wrapper script with the shebang including the `env`. I did create a Vagrantfile to reproduce this issue: https://gist.github.com/wheelerlaw/3d852c6a0e5122f050e64541101d3aac – wheeler Apr 19 '23 at 15:27
  • Well, another take. `subprocess.run` is documented to inherit its parent's standard I/O FDs by default («With the default settings of `None`, no redirection will occur; the child’s file handles will be inherited from the parent.»); using `capture_output=True` will make `stdout` and `stderr` to be connected to internal pipes, but that do nothing to `stdin`. In contrast, Go's `exec.Cmd` has its fields for the standard I/O descriptors naturally initialized to `nil` (unless overridden) which means the corresponding FDs of the process are connected to `/dev/null`, which… – kostix Apr 19 '23 at 19:16
  • …means the process' stdin gets also connected to `/dev/null` as `exec.Command` is documented as «It sets only the Path and Args in the returned structure.». Calling `Output` sets the `Stdout` field before calling `Run` (and also `Stderr` if it was not overridden already) but does not modify `Stdin`. What I propose: try first to explicitly connect `Stdin` of the created process to the stdin of the current process with `op.Stdin = os.Stdin`, and see whether that works. – kostix Apr 19 '23 at 19:26
  • Unfortunately, it did not have an effect. And just to clarify, [here](https://gist.github.com/wheelerlaw/3d852c6a0e5122f050e64541101d3aac#file-vagrantfile-L43) is the line added to the small Go program. – wheeler Apr 19 '23 at 21:35
  • But this seems expected, because in the case of `subprocess.run` inheriting the parent's standard I/O FDs, the parent in this case _is_ the Go app, which as you said, `exec.Command` sets the child's `Stdin` to `nil` unless overridden. So that would mean that the Python wrapper that gets called from the Go app inherits the parent's `/dev/null` for `stdin`. If the lack of a usable FD for `stdin` were to be the cause of the issue, then I would have expected the Go app to fail regardless of whether it calls the 1Password CLI directly (`op`) or calls it through the Python wrapper script (`op.py`). – wheeler Apr 19 '23 at 21:36
  • I just tried the 1password cli and it "works on my machine" with the same code you posted. `op --version 2.15.0` – Vüsal Apr 20 '23 at 13:05
  • What kind of system are you running? This isn't something local to my system, as I was able to reproduce the issue in a VM. [Here](https://gist.github.com/wheelerlaw/3d852c6a0e5122f050e64541101d3aac) is a link to the Vagrantfile. – wheeler Apr 20 '23 at 15:26
  • `ProductName: macOS ProductVersion: 13.0 BuildVersion: 22A380` - might be that the linux version of the binary is somehow broken? Do you run it locally on linux? – Vüsal Apr 25 '23 at 16:24

3 Answers3

2

I had the same problem accessing op from a pyenv virtual environment. I figured that the problem is that the pyenv's python executable is owned by the user (me). Changing the ownership to root:root of the python interpreter and the directory that it lives in actually helped. Not sure what is going on behind the scenes.

Here are the steps (I use the --copies to create a virtual environment, so it is not using symlinks - as symlinks would point back to root owned files):

$ python -m venv --copies .venv
$ .venv/bin/python -c "import subprocess; subprocess.call(['op', 'signin'])"
[ERROR] 2023/05/16 16:01:18 connecting to desktop app: read: connection reset, make sure the CLI is correctly installed and Connect with 1Password CLI is enabled in the 1Password app
# chown -R root:root .venv/
$ .venv/bin/python -c "import subprocess; subprocess.call(['op', 'signin'])"
 Select account  [Use arrows to move, type to filter]

Bottomline: Change the ownership of your executable (and directory it lives in) that spawns the op subprocess to root:root

Also please see this thread on 1Password that look like the same issue:

https://1password.community/discussion/135768/calling-the-cli-from-a-go-program

Mate Lakat
  • 36
  • 3
  • 1
    Can you please show the result of calling `stat` on `.venv/bin/python` (I'm only interested in permission bits)? – kostix May 16 '23 at 14:51
  • Oh my goodness, this is it. But not only does the executable need to be owned by `root:root`, but also the directory that the executable is in. – wheeler May 16 '23 at 19:33
  • Which makes sense why the system Python interpreter worked because its owned by root and located in `/usr/bin/` which is also owned by root. And same for running `op` from the terminal using Bash. – wheeler May 16 '23 at 19:42
  • I think this has something to do with the permissions of the `op` binary as well. I noticed that the setgid bit was set and running `stat` on `/usr/bin/op` revealed that the group is set to `onepassword-cli`. If I try to run the `op` binary under my user and group, I get the exact same error as what's in the question. – wheeler May 16 '23 at 20:46
  • I'm also going to edit this answer a tiny bit to include that the directory as well needs to be owned by root, but I'll mark it as accepted. Not bad for your first contribution :) – wheeler May 16 '23 at 20:49
  • My colleague has found a discussion on 1Password that reveals [exactly the same problem](https://1password.community/discussion/135768/calling-the-cli-from-a-go-program) I am adding it to the answer as well. – Mate Lakat May 18 '23 at 10:07
1

This issue was fixed in version 8.10.8 of the 1Password app. You should now be able to run your scripts without any workarounds.

Joris
  • 862
  • 1
  • 8
  • 17
-2

I'm actually using GoLang as an overlaying GUI application which communicates with several distinct cpp binaries as a centralised controller. I've implemented the following function to run generic commands:

func RunCmd(binary string, cmd []string) []string {
    var output []string
    c, b := exec.Command(binary, cmd...), new(bytes.Buffer)
    c.Stdout = b
    c.Run()
    s := bufio.NewScanner(b)
    for s.Scan() {
        output = append(output, s.Text())
    }
    return output
}

This function runs the command and returns the output as a slice of strings (one per line of output). Here's an example of calling it:

_ = RunCmd("./moveRobot", []string{"10.20.0.37", localIP.String(), "align"})

In your example, you would call it as:

out = RunCmd("op", []string{"item", "get", "DocSend", "--format=json"})

It is also possible that you need to provide the path to the op binary however, so I would try that (if it's in the same directory, then use ./op

This obviously doesn't return the output in JSON as is, but you can use it as a starting point to at least confirm the call is working.

Xarus
  • 6,589
  • 4
  • 15
  • 22
  • I am not having an issue with `exec.Command` with anything else. Now am I having an issue calling the 1Password CLI from any other language. It is just the combination of `op` and Golang what is giving me issues. – wheeler Apr 18 '23 at 19:18
  • This doesn't solve the problem. – Vüsal Apr 19 '23 at 08:37
  • @wheeler Where is the op binary located? If it's in the local directory you'll need to use `./op` to call it, and if it's using environment variables then you'll possibly need to use dotenv to set them – Xarus Apr 19 '23 at 18:40
  • That's not really a relevant question as I am not having an issue with the Go app _finding_ the 1Password CLI binary `op`. It can find it just fine because when I call the `op` binary from the Go app without any arguments, the `op` binary correctly runs, returning the help information to the Go app, which is then printed to stdout by the Go app. – wheeler Apr 19 '23 at 21:40
  • @wheeler Ah I see, sorry I hadn't understood that you'd tried to run just the op binary without arguments. Your error message is giving you exit code 1, which implies (based on this support answer on 1password's site: https://1password.community/discussion/101297/are-there-exit-codes-in-op ) that it's not authenticating to the app properly. Is it possible that the Go exec shell doesn't have access to some environment variables that are needed? Try this answer to make sure that the environment is using your OS env: https://stackoverflow.com/questions/41133115/pass-env-var-to-exec-command – Xarus Apr 19 '23 at 22:03