Skip to content

Commit 42db3a1

Browse files
authored
Merge pull request #33 from automagik-dev/fix/headless-keyring-setup
fix(auth): probe SecretService + headless-first docs
2 parents 1791f56 + 23086ce commit 42db3a1

4 files changed

Lines changed: 88 additions & 27 deletions

File tree

install.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,8 @@ if [ ! -t 0 ]; then
415415
printf "\n"
416416
printf "Next steps:\n"
417417
printf " 1. wk auth status → check existing accounts\n"
418-
printf " 2. wk auth manage → connect Google account (prints URL)\n"
418+
printf " 2. wk auth add user@example.com --headless → prints auth URL, polls for token\n"
419+
printf " (keyring auto-configured on headless Linux — no manual setup needed)\n"
419420
printf " 3. wk gmail search 'newer_than:1d' --json → first query\n"
420421
printf "\n"
421422
printf "Available services: Gmail, Calendar, Drive, Sheets, Docs, Slides,\n"

internal/setup/keyring.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ package setup
44

55
import (
66
"bufio"
7+
"context"
78
"crypto/rand"
89
"encoding/hex"
910
"fmt"
1011
"io"
1112
"os"
13+
"os/exec"
1214
"path/filepath"
1315
"runtime"
1416
"strings"
17+
"time"
1518

1619
"github.com/automagik-dev/workit/internal/config"
1720
"github.com/automagik-dev/workit/internal/secrets"
@@ -41,8 +44,9 @@ var (
4144
// NeedsFileBackendSetup reports whether the current environment will use
4245
// the keyring file backend and therefore needs automatic password setup.
4346
//
44-
// Returns true on Linux when the resolved backend is "auto" and no
45-
// DBUS_SESSION_BUS_ADDRESS is set (headless environment).
47+
// Returns true on Linux when the resolved backend is "auto" and either
48+
// no DBUS_SESSION_BUS_ADDRESS is set, or D-Bus is present but the
49+
// org.freedesktop.secrets service (gnome-keyring/KeePassXC) is not available.
4650
func NeedsFileBackendSetup(goos string, dbusAddr string) (bool, error) {
4751
if goos != "linux" {
4852
return false, nil
@@ -59,19 +63,52 @@ func NeedsFileBackendSetup(goos string, dbusAddr string) (bool, error) {
5963
return false, nil
6064
}
6165

62-
// D-Bus present means SecretService/gnome-keyring is likely available.
63-
if dbusAddr != "" {
64-
return false, nil
66+
// No D-Bus at all → definitely needs file backend.
67+
if dbusAddr == "" {
68+
return true, nil
69+
}
70+
71+
// D-Bus is present, but SecretService might not be running
72+
// (common on headless servers with systemd but no gnome-keyring).
73+
// Probe for the org.freedesktop.secrets name on the session bus.
74+
if !isSecretServiceAvailable() {
75+
return true, nil
6576
}
6677

67-
return true, nil
78+
return false, nil
79+
}
80+
81+
// secretServiceProbeTimeout is the maximum time to wait for dbus-send to respond.
82+
const secretServiceProbeTimeout = 2 * time.Second
83+
84+
// isSecretServiceAvailable checks whether the org.freedesktop.secrets D-Bus
85+
// service is registered on the session bus. Returns false if dbus-send is not
86+
// installed or the service is not available.
87+
var isSecretServiceAvailable = func() bool {
88+
ctx, cancel := context.WithTimeout(context.Background(), secretServiceProbeTimeout)
89+
defer cancel()
90+
91+
cmd := exec.CommandContext(ctx, "dbus-send",
92+
"--session",
93+
"--dest=org.freedesktop.DBus",
94+
"--type=method_call",
95+
"--print-reply",
96+
"/org/freedesktop/DBus",
97+
"org.freedesktop.DBus.NameHasOwner",
98+
"string:org.freedesktop.secrets",
99+
)
100+
out, err := cmd.Output()
101+
if err != nil {
102+
return false
103+
}
104+
return strings.Contains(string(out), "true")
68105
}
69106

70107
// SetupKeyringIfNeeded detects whether the file keyring backend will be used
71108
// and, if so, generates a random password, saves it to keyring.key, writes
72109
// credentials.env, and configures the user's shell profile to source it.
73110
//
74-
// On macOS or Linux with D-Bus present, this is a no-op.
111+
// On macOS or Linux with a working SecretService, this is a no-op.
75112
// If keyring.key already exists, the existing password is reused.
76113
//
77114
// Status messages are written to w (typically os.Stderr).

internal/setup/keyring_test.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,31 @@ import (
1111
)
1212

1313
func TestNeedsFileBackendSetup(t *testing.T) {
14-
// Save and restore the original resolveBackendInfo.
14+
// Save and restore the original stubs.
1515
origResolve := resolveBackendInfo
16+
origProbe := isSecretServiceAvailable
1617

17-
t.Cleanup(func() { resolveBackendInfo = origResolve })
18+
t.Cleanup(func() {
19+
resolveBackendInfo = origResolve
20+
isSecretServiceAvailable = origProbe
21+
})
1822

1923
// Mock: return "auto" backend with "default" source (typical fresh install).
2024
resolveBackendInfo = func() (secrets.KeyringBackendInfo, error) {
2125
return secrets.KeyringBackendInfo{Value: "auto", Source: "default"}, nil
2226
}
2327

28+
// Mock: SecretService is available (for dbus-present cases).
29+
isSecretServiceAvailable = func() bool { return true }
30+
2431
tests := []struct {
2532
name string
2633
goos string
2734
dbusAddr string
2835
want bool
2936
}{
3037
{"linux headless", "linux", "", true},
31-
{"linux with dbus", "linux", "/run/user/1000/bus", false},
38+
{"linux with dbus and secret service", "linux", "/run/user/1000/bus", false},
3239
{"darwin", "darwin", "", false},
3340
{"darwin with dbus", "darwin", "/tmp/bus", false},
3441
{"windows", "windows", "", false},
@@ -46,6 +53,20 @@ func TestNeedsFileBackendSetup(t *testing.T) {
4653
}
4754
})
4855
}
56+
57+
// D-Bus present but SecretService NOT running → needs file backend.
58+
t.Run("linux with dbus but no secret service", func(t *testing.T) {
59+
isSecretServiceAvailable = func() bool { return false }
60+
61+
got, err := NeedsFileBackendSetup("linux", "/run/user/1000/bus")
62+
if err != nil {
63+
t.Fatalf("unexpected error: %v", err)
64+
}
65+
66+
if !got {
67+
t.Error("expected true when D-Bus present but SecretService unavailable")
68+
}
69+
})
4970
}
5071

5172
func TestNeedsFileBackendSetup_ExplicitBackend(t *testing.T) {
@@ -371,11 +392,13 @@ func TestSetupKeyringIfNeeded_NoOp_LinuxWithDBus(t *testing.T) {
371392
origGOOS := runtimeGOOS
372393
origResolve := resolveBackendInfo
373394
origGetenv := getenv
395+
origProbe := isSecretServiceAvailable
374396

375397
t.Cleanup(func() {
376398
runtimeGOOS = origGOOS
377399
resolveBackendInfo = origResolve
378400
getenv = origGetenv
401+
isSecretServiceAvailable = origProbe
379402
})
380403

381404
runtimeGOOS = "linux"
@@ -389,6 +412,8 @@ func TestSetupKeyringIfNeeded_NoOp_LinuxWithDBus(t *testing.T) {
389412

390413
return ""
391414
}
415+
// Mock SecretService as available so setup is a no-op.
416+
isSecretServiceAvailable = func() bool { return true }
392417

393418
var buf bytes.Buffer
394419
if err := SetupKeyringIfNeeded(&buf); err != nil {

plugins/workit/skills/setup.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,28 @@ Check: `wk version` and `wk auth status`
1919

2020
## Auth flows by environment
2121

22-
### Desktop / laptop
22+
### Agent / automation / headless server (recommended)
2323
```bash
24-
wk auth manage # opens browser, auto-closes after login
24+
wk auth add user@example.com --headless
25+
# Prints a Google login URL → user opens it in any browser
26+
# CLI polls auth.automagik.dev until token arrives, then stores it
27+
# Keyring auto-configured on headless Linux — no manual setup needed
2528
```
2629

27-
### Remote server / VPS (SSH headless)
30+
### Two-step (scripting / no-poll)
2831
```bash
29-
wk auth manage # detects no TTY, prints URL with server outbound IP
30-
# Open printed URL in your browser — auth completes automatically
32+
wk auth add user@example.com --headless --no-poll
33+
# Prints URL + state, returns immediately
34+
wk auth poll <state>
35+
# Polls and stores token when ready
3136
```
3237

33-
### Agent / automation (fully unattended)
34-
```bash
35-
wk auth add user@example.com --headless --no-input
36-
# Prints a Google login URL. User (or automation) opens it.
37-
# CLI polls auth.automagik.dev until token arrives, then stores it.
38-
```
39-
40-
### Get just the URL (for scripting)
38+
### Desktop / laptop
4139
```bash
42-
wk auth manage --print-url # prints JSON: {"url":"https://...","state":"..."}
40+
wk auth manage # opens browser, auto-closes after login
4341
```
4442

45-
**Linux headless keyring:** auto-configured. No manual setup or `source` needed after v2.260227.4+.
43+
**Linux headless keyring:** auto-configured. No manual setup or `source` needed after v2.260303.2+.
4644

4745
---
4846

@@ -101,7 +99,7 @@ wk auth service-account unset
10199

102100
## 8) Recommended pattern in agents
103101
1. `wk auth status` — check if account already exists
104-
2. If not: `wk auth manage`opens auth (prints URL on headless, browser otherwise)
102+
2. If not: `wk auth add user@example.com --headless`prints auth URL, polls until done
105103
3. `wk auth services` — verify services are authorized
106104
4. Read operations: add `--read-only`
107105
5. Write operations: `--dry-run` first, then without after confirmation

0 commit comments

Comments
 (0)