sido-askpass 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -29
- package/dist/sido-askpass.js +288 -93
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,65 +1,185 @@
|
|
|
1
1
|
# sido-askpass
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Name:** sido-askpass
|
|
4
|
+
**Author:** patdx
|
|
5
|
+
**Repo:** https://github.com/patdx/sido-askpass
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`SUDO_ASKPASS` shim for tmux, Herdr, coding agents, and other environments
|
|
8
|
+
without a usable TTY.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
When sudo uses askpass, `sido-askpass` opens a hidden password prompt and
|
|
11
|
+
returns the password to sudo. Passwords never touch a regular file.
|
|
12
|
+
|
|
13
|
+
Works with:
|
|
14
|
+
|
|
15
|
+
- **Linux** — hidden GUI or terminal password prompts
|
|
16
|
+
- **macOS** — hidden native password dialog
|
|
17
|
+
- **Codex automatic review** — approved retries can authenticate outside the sandbox
|
|
18
|
+
- **Codex Full Access** — sudo authentication from agent-run commands
|
|
19
|
+
- **tmux** — hidden popup prompt without stealing the agent's TTY
|
|
20
|
+
- **Herdr** — temporary password prompt pane that closes automatically
|
|
21
|
+
- **Raw terminals** — `/dev/tty` fallback when no GUI or multiplexer is available
|
|
22
|
+
|
|
23
|
+
Supports Linux and macOS. Requires **Node.js 24+**. Bash is required for the
|
|
24
|
+
tmux, Herdr, and `/dev/tty` backends; tmux and Herdr also require `mkfifo`.
|
|
25
|
+
Linux GUI prompting requires either `zenity` or `kdialog`. Windows is not
|
|
26
|
+
supported.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
### User setup (recommended)
|
|
31
|
+
|
|
32
|
+
Install the command, add `SUDO_ASKPASS` to `~/.profile`, and load it into the
|
|
33
|
+
current shell:
|
|
8
34
|
|
|
9
35
|
```bash
|
|
10
|
-
npm
|
|
11
|
-
# or: pnpm add -g sido-askpass
|
|
36
|
+
npm install -g sido-askpass && sido-askpass install --user && . ~/.profile
|
|
12
37
|
```
|
|
13
38
|
|
|
14
|
-
|
|
39
|
+
If `.profile` already exports another `SUDO_ASKPASS`, installation prints the
|
|
40
|
+
old and new values before replacing it. Uninstall removes only the entry marked
|
|
41
|
+
as managed by sido.
|
|
42
|
+
|
|
43
|
+
### System setup
|
|
44
|
+
|
|
45
|
+
Install the command and configure `Path askpass` in `/etc/sudo.conf`:
|
|
15
46
|
|
|
16
|
-
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g sido-askpass && sido-askpass install --system
|
|
49
|
+
```
|
|
17
50
|
|
|
18
|
-
|
|
51
|
+
System setup runs `sudo tee` to update `/etc/sudo.conf`, so the installation
|
|
52
|
+
command itself must be run somewhere sudo can authenticate.
|
|
53
|
+
|
|
54
|
+
For a single session without changing a profile or sudo configuration:
|
|
19
55
|
|
|
20
56
|
```bash
|
|
21
57
|
export SUDO_ASKPASS=$(command -v sido-askpass)
|
|
22
58
|
```
|
|
23
59
|
|
|
24
|
-
|
|
60
|
+
Alternatively, inject `SUDO_ASKPASS` for one command:
|
|
25
61
|
|
|
26
62
|
```bash
|
|
27
|
-
sido-askpass
|
|
28
|
-
# or
|
|
29
|
-
sido-askpass install --system # /etc/sudo.conf → Path askpass
|
|
63
|
+
sido-askpass run sudo -A <command>
|
|
30
64
|
```
|
|
31
65
|
|
|
32
|
-
|
|
66
|
+
Everything after `run` is run directly without shell parsing. `run` sets
|
|
67
|
+
`SUDO_ASKPASS` but does not add `-A` or otherwise rewrite the command, so pass
|
|
68
|
+
`-A` explicitly when sudo must use askpass even if a TTY is available.
|
|
69
|
+
|
|
70
|
+
## Using sudo
|
|
71
|
+
|
|
72
|
+
For the most reliable behavior, explicitly tell sudo to use askpass:
|
|
33
73
|
|
|
34
74
|
```bash
|
|
35
75
|
sudo -A <command>
|
|
36
76
|
```
|
|
37
77
|
|
|
38
|
-
On modern
|
|
78
|
+
On modern sudo installations, plain sudo commonly falls back to askpass when
|
|
79
|
+
there is no usable TTY. This automatic fallback has been verified on Fedora:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
sudo <command>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
TTY allocation is the important distinction:
|
|
86
|
+
|
|
87
|
+
- Without a TTY, plain sudo can automatically use the configured askpass
|
|
88
|
+
helper.
|
|
89
|
+
- With a real or pseudo-TTY, plain sudo prompts on that terminal instead and
|
|
90
|
+
does not call askpass.
|
|
91
|
+
- Some coding agents start background commands with a pseudo-TTY, so automatic
|
|
92
|
+
fallback cannot be assumed even when the agent UI has no visible terminal.
|
|
93
|
+
- `sudo -A` requests askpass regardless of whether a TTY exists.
|
|
94
|
+
|
|
95
|
+
For interactive shells, an optional alias can make askpass the default:
|
|
39
96
|
|
|
40
97
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
98
|
+
alias sudo='sudo -A'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Shell aliases usually do not affect commands launched directly by coding
|
|
102
|
+
agents or other non-interactive processes. `sido-askpass` may automate this in
|
|
103
|
+
the future, but it currently does not install an alias, rewrite sudo commands,
|
|
104
|
+
or add `-A`.
|
|
105
|
+
|
|
106
|
+
## Codex test matrix
|
|
107
|
+
|
|
108
|
+
| Platform and terminal | Codex permissions | Prompt backend | Command | Result | Notes |
|
|
109
|
+
| ---------------------------- | ----------------- | -------------- | -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| Linux, tmux 3.7b | Automatic review | tmux popup | `sudo -A` | Tested | The sandbox cannot reach tmux; the approved retry outside the sandbox succeeds. |
|
|
111
|
+
| Linux, Herdr 0.7.5 | Automatic review | Herdr pane | `sudo -A` | Tested | The sandbox cannot reach Herdr; the approved retry outside the sandbox succeeds. |
|
|
112
|
+
| Fedora 44, raw Wayland shell | Automatic review | Zenity 4.2.2 | `sudo -A true` | Tested | 2026-07-30, sudo 1.9.17p2, Node.js 24.18.0; no tmux, Herdr, or stdin TTY. |
|
|
113
|
+
| Linux, any terminal | Full Access | First matching | `sudo -A` | Supported | sudo and the selected prompt backend can run outside the sandbox. |
|
|
114
|
+
| Linux, any terminal | Workspace-write | None | Any sudo | Unsupported | `no_new_privileges` blocks sudo; changing the askpass backend cannot bypass this. |
|
|
115
|
+
| Linux, pseudo-TTY command | Any sudo-capable | Askpass | Plain `sudo` | Skipped | sudo uses the allocated TTY instead of askpass; use `sudo -A` to force askpass. |
|
|
116
|
+
| Linux, raw terminal | Automatic review | `/dev/tty` | `sudo -A` | Untested | No tmux, Herdr, `DISPLAY`, or Wayland; requires an accessible controlling terminal. |
|
|
117
|
+
| Linux, pi (no TTY, GUI) | Automatic review | Zenity | Plain `sudo` | Tested | 2026-07-30, sudo 1.9.17p2, Node.js 24.18.0: no tmux, Herdr, or TTY; `DISPLAY`+zenity available. Plain sudo auto-fell back to askpass. |
|
|
118
|
+
| macOS | Any | AppleScript | `sudo -A` | Untested | The dialog backend exists, but Codex sandbox and approval behavior has not been tested. |
|
|
119
|
+
|
|
120
|
+
Codex automatic approval review does not widen the sandbox itself. It is useful
|
|
121
|
+
because it can approve an eligible retry outside the sandbox. Full Access also
|
|
122
|
+
works, but grants Codex substantially broader system access.
|
|
123
|
+
|
|
124
|
+
When a sandboxed Linux Codex process cannot reach tmux, `sido-askpass` exits
|
|
125
|
+
immediately instead of hanging and prints:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
[sido] tmux access denied; retry the sudo command with escalated permissions
|
|
43
129
|
```
|
|
44
130
|
|
|
45
|
-
|
|
131
|
+
This gives Codex an actionable reason to retry with escalated permission.
|
|
132
|
+
|
|
133
|
+
## Other clients
|
|
134
|
+
|
|
135
|
+
| Client | Compatibility | Notes |
|
|
136
|
+
| -------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| Pi | Tested | No TTY — plain `sudo` auto-falls back to askpass; zenity GUI prompt. 2026-07-30, Fedora 44, Node.js 24.18.0, sudo 1.9.17p2. |
|
|
138
|
+
| OpenCode | Tested | 2026-07-30, Fedora 44, Node.js 24.18.0, `/dev/tty` fallback, `sudo -A whoami` → `root`. |
|
|
139
|
+
|
|
140
|
+
## Prompt backend compatibility
|
|
46
141
|
|
|
47
|
-
|
|
48
|
-
| ---------------------------- | ------------------------------------------------------ |
|
|
49
|
-
| `$TMUX` set | `tmux command-prompt -N` (secret), FIFO back to parent |
|
|
50
|
-
| `$HERDR_ENV=1` | Herdr pane split + bash `read -s`, FIFO back |
|
|
51
|
-
| macOS + GUI | `osascript` hidden dialog |
|
|
52
|
-
| Linux + `$DISPLAY` / Wayland | `zenity` → `kdialog` |
|
|
53
|
-
| else | `read -s` on `/dev/tty` |
|
|
142
|
+
The first matching context is used:
|
|
54
143
|
|
|
55
|
-
|
|
144
|
+
| Context | Prompt method | Status |
|
|
145
|
+
| ---------------------------- | ------------------------------------------------ | ----------------------- |
|
|
146
|
+
| `$TMUX` set | tmux popup with Bash `read -s`; FIFO return | Tested on Linux |
|
|
147
|
+
| `$HERDR_ENV=1` | Temporary Herdr pane with `read -s`; FIFO return | Tested on Linux |
|
|
148
|
+
| macOS | Hidden `osascript` dialog | Implemented, unverified |
|
|
149
|
+
| Linux + `$DISPLAY` / Wayland | `zenity`, then `kdialog` | Tested on Linux |
|
|
150
|
+
| Other environment | Hidden `read -s` prompt on `/dev/tty` | Fallback |
|
|
56
151
|
|
|
57
|
-
|
|
152
|
+
The prompt also shows the requesting command when the parent process command
|
|
153
|
+
line is available. If process inspection is blocked, it silently shows the
|
|
154
|
+
normal password prompt without the command. The displayed command includes its
|
|
155
|
+
arguments, which may reveal sensitive arguments to anyone who can see the
|
|
156
|
+
prompt.
|
|
58
157
|
|
|
59
|
-
|
|
158
|
+
## Status and removal
|
|
159
|
+
|
|
160
|
+
Inspect the active environment and installed configuration:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
sido-askpass status
|
|
164
|
+
sido-askpass status --user
|
|
165
|
+
sido-askpass status --system
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Remove either setup:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
sido-askpass uninstall --user
|
|
172
|
+
sido-askpass uninstall --system
|
|
173
|
+
```
|
|
60
174
|
|
|
61
|
-
|
|
175
|
+
`--user` and `--system` are mutually exclusive for install, uninstall, and
|
|
176
|
+
status. Omitting both is supported only by status.
|
|
62
177
|
|
|
63
178
|
## Security
|
|
64
179
|
|
|
65
|
-
|
|
180
|
+
Password input is hidden. tmux and Herdr return the password through a kernel
|
|
181
|
+
FIFO; GUI and TTY backends return it through process stdout. Each tmux or Herdr
|
|
182
|
+
prompt uses a private temporary directory. Its FIFO, prompt, and helper script
|
|
183
|
+
are accessible only to the current user and are removed when prompting ends.
|
|
184
|
+
The temporary prompt file contains the displayed command and prompt, but never
|
|
185
|
+
the password.
|
package/dist/sido-askpass.js
CHANGED
|
@@ -3,19 +3,53 @@
|
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
6
|
+
mkdtempSync,
|
|
6
7
|
readFileSync,
|
|
8
|
+
rmSync,
|
|
7
9
|
writeFileSync,
|
|
8
|
-
unlinkSync,
|
|
9
|
-
chmodSync,
|
|
10
10
|
} from 'node:fs'
|
|
11
11
|
import { tmpdir, userInfo, hostname, homedir } from 'node:os'
|
|
12
12
|
import { join, resolve } from 'node:path'
|
|
13
|
-
import { randomUUID } from 'node:crypto'
|
|
14
13
|
import { parseArgs } from 'node:util'
|
|
14
|
+
import package_json from '../package.json' with { type: 'json' }
|
|
15
15
|
|
|
16
16
|
const self_path = resolve(process.argv[1] )
|
|
17
17
|
const command = process.argv[2]
|
|
18
18
|
|
|
19
|
+
if (command === '_inner_prompt_receiver') {
|
|
20
|
+
const prompt_file = process.argv[3]
|
|
21
|
+
const fifo = process.argv[4]
|
|
22
|
+
if (!prompt_file || !fifo) process.exit(1)
|
|
23
|
+
inner_prompt_receiver(prompt_file, fifo)
|
|
24
|
+
process.exit(0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── help / version ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
30
|
+
console.log(`sido-askpass — SUDO_ASKPASS shim for headless agent environments
|
|
31
|
+
|
|
32
|
+
Name: sido-askpass
|
|
33
|
+
Version: ${package_json.version}
|
|
34
|
+
Author: patdx
|
|
35
|
+
Repo: https://github.com/patdx/sido-askpass
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
sido-askpass askpass mode (invoked by sudo)
|
|
39
|
+
sido-askpass install --user|--system
|
|
40
|
+
sido-askpass uninstall --user|--system
|
|
41
|
+
sido-askpass status [--user|--system]
|
|
42
|
+
sido-askpass run <command> [args...] run <command> with SUDO_ASKPASS set
|
|
43
|
+
sido-askpass --help
|
|
44
|
+
sido-askpass --version`)
|
|
45
|
+
process.exit(0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (command === '--version' || command === '-V' || command === 'version') {
|
|
49
|
+
console.log(package_json.version)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
// ── install / uninstall / status ────────────────────────────────────────────
|
|
20
54
|
|
|
21
55
|
if (command === 'install' || command === 'uninstall') {
|
|
@@ -43,32 +77,77 @@ if (command === 'status') {
|
|
|
43
77
|
user: { type: 'boolean' },
|
|
44
78
|
system: { type: 'boolean' },
|
|
45
79
|
},
|
|
46
|
-
strict: false,
|
|
47
80
|
})
|
|
81
|
+
if (values.user && values.system) {
|
|
82
|
+
console.error(`[sido] usage: ${self_path} status [--user|--system]`)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
48
85
|
const scope = values.user ? '--user' : values.system ? '--system' : undefined
|
|
49
86
|
do_status(scope)
|
|
50
87
|
process.exit(0)
|
|
51
88
|
}
|
|
52
89
|
|
|
90
|
+
if (command === 'run') {
|
|
91
|
+
if (!process.argv[3]) {
|
|
92
|
+
console.error(`[sido] usage: ${self_path} run <command> [args...]`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
do_run(process.argv[3], process.argv.slice(4))
|
|
96
|
+
}
|
|
97
|
+
|
|
53
98
|
// ── askpass mode ───────────────────────────────────────────────────────────
|
|
54
99
|
|
|
55
100
|
const prompt = (command || `[sudo] password for ${userInfo().username}: `)
|
|
56
101
|
.replace(/%u/g, userInfo().username)
|
|
57
102
|
.replace(/%h/g, hostname())
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
const requesting_command = get_requesting_command()
|
|
104
|
+
const display_prompt = requesting_command
|
|
105
|
+
? `Command: ${requesting_command}\n${prompt}`
|
|
106
|
+
: prompt
|
|
107
|
+
|
|
108
|
+
const is_tmux = !!process.env.TMUX
|
|
109
|
+
const is_herdr = process.env.HERDR_ENV === '1'
|
|
110
|
+
const is_mac = process.platform === 'darwin'
|
|
111
|
+
const is_linux = process.platform === 'linux'
|
|
63
112
|
const has_display = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)
|
|
64
|
-
const can_gui =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
const can_gui = is_mac || (is_linux && has_display)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
function create_prompt_resources() {
|
|
122
|
+
const dir = mkdtempSync(join(tmpdir(), 'sido-'))
|
|
123
|
+
const fifo = join(dir, 'password')
|
|
124
|
+
const result = spawnSync('mkfifo', ['-m', '600', fifo], {
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
+
})
|
|
127
|
+
if (result.error || result.status !== 0) {
|
|
128
|
+
rmSync(dir, { recursive: true, force: true })
|
|
129
|
+
const detail =
|
|
130
|
+
result.error?.message ||
|
|
131
|
+
result.stderr?.toString().trim() ||
|
|
132
|
+
'unknown error'
|
|
133
|
+
throw new Error(`mkfifo failed: ${detail}`)
|
|
134
|
+
}
|
|
135
|
+
const resources = {
|
|
136
|
+
dir,
|
|
137
|
+
fifo,
|
|
138
|
+
prompt_file: join(dir, 'prompt'),
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(resources.prompt_file, display_prompt, { mode: 0o600 })
|
|
142
|
+
return resources
|
|
143
|
+
} catch (error) {
|
|
144
|
+
rmSync(dir, { recursive: true, force: true })
|
|
145
|
+
throw error
|
|
146
|
+
}
|
|
68
147
|
}
|
|
69
148
|
|
|
70
|
-
function
|
|
71
|
-
|
|
149
|
+
function remove_prompt_resources(resources ) {
|
|
150
|
+
rmSync(resources.dir, { recursive: true, force: true })
|
|
72
151
|
}
|
|
73
152
|
|
|
74
153
|
function read_stdout(r
|
|
@@ -79,9 +158,56 @@ function read_stdout(r
|
|
|
79
158
|
return r.stdout.toString().replace(/\r?\n$/, '')
|
|
80
159
|
}
|
|
81
160
|
|
|
161
|
+
function read_secret(prompt ) {
|
|
162
|
+
const r = spawnSync(
|
|
163
|
+
'bash',
|
|
164
|
+
[
|
|
165
|
+
'-c',
|
|
166
|
+
'read -s -p "$1" pw < /dev/tty && printf \'%s\\n\' "$pw"',
|
|
167
|
+
'sido',
|
|
168
|
+
prompt,
|
|
169
|
+
],
|
|
170
|
+
{ stdio: ['ignore', 'pipe', 'inherit'] },
|
|
171
|
+
)
|
|
172
|
+
return read_stdout(r)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function inner_prompt_receiver(prompt_file , fifo ) {
|
|
176
|
+
const prompt = readFileSync(prompt_file, 'utf8')
|
|
177
|
+
writeFileSync(fifo, `${read_secret(prompt)}\n`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function get_requesting_command() {
|
|
181
|
+
try {
|
|
182
|
+
let value
|
|
183
|
+
if (process.platform === 'linux') {
|
|
184
|
+
value = readFileSync(`/proc/${process.ppid}/cmdline`, 'utf8').replace(
|
|
185
|
+
/\0/g,
|
|
186
|
+
' ',
|
|
187
|
+
)
|
|
188
|
+
} else {
|
|
189
|
+
const r = spawnSync(
|
|
190
|
+
'ps',
|
|
191
|
+
['-o', 'command=', '-p', String(process.ppid)],
|
|
192
|
+
{ stdio: ['pipe', 'pipe', 'pipe'] },
|
|
193
|
+
)
|
|
194
|
+
if (r.status !== 0) return
|
|
195
|
+
value = r.stdout?.toString() ?? ''
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
value = value
|
|
199
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
|
200
|
+
.replace(/\s+/g, ' ')
|
|
201
|
+
.trim()
|
|
202
|
+
return value || undefined
|
|
203
|
+
} catch {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
82
208
|
function main() {
|
|
83
|
-
if (
|
|
84
|
-
if (
|
|
209
|
+
if (is_tmux) return void tmux_prompt()
|
|
210
|
+
if (is_herdr) return void herdr_prompt()
|
|
85
211
|
if (can_gui) return void gui_prompt()
|
|
86
212
|
tty_prompt()
|
|
87
213
|
}
|
|
@@ -94,15 +220,41 @@ function user_profile_path() {
|
|
|
94
220
|
return join(homedir(), '.profile')
|
|
95
221
|
}
|
|
96
222
|
|
|
223
|
+
function do_run(command , args ) {
|
|
224
|
+
const proc = spawnSync(command, args, {
|
|
225
|
+
env: { ...process.env, SUDO_ASKPASS: self_path },
|
|
226
|
+
stdio: 'inherit',
|
|
227
|
+
})
|
|
228
|
+
if (proc.error) {
|
|
229
|
+
console.error(`[sido] failed to run ${command}: ${proc.error.message}`)
|
|
230
|
+
process.exit(1)
|
|
231
|
+
}
|
|
232
|
+
if (proc.signal) {
|
|
233
|
+
process.kill(process.pid, proc.signal)
|
|
234
|
+
}
|
|
235
|
+
process.exit(proc.status ?? 1)
|
|
236
|
+
}
|
|
237
|
+
|
|
97
238
|
function do_install(scope ) {
|
|
98
239
|
if (scope === '--user') {
|
|
99
240
|
const path = user_profile_path()
|
|
100
241
|
const line = `export SUDO_ASKPASS="${self_path}"`
|
|
101
242
|
let content = existsSync(path) ? readFileSync(path, 'utf-8') + '\n' : ''
|
|
102
|
-
|
|
103
|
-
|
|
243
|
+
const managed_pattern = /^# sido\nexport SUDO_ASKPASS=.*$/gm
|
|
244
|
+
if (managed_pattern.test(content)) {
|
|
245
|
+
content = content.replace(managed_pattern, `# sido\n${line}`)
|
|
246
|
+
} else if (/^export SUDO_ASKPASS=.*$/m.test(content)) {
|
|
247
|
+
const old_values = [
|
|
248
|
+
...content.matchAll(/^export SUDO_ASKPASS=(.*)$/gm),
|
|
249
|
+
].map((match) => match[1])
|
|
250
|
+
for (const old_value of old_values) {
|
|
251
|
+
console.error(
|
|
252
|
+
`[sido] warning: replacing SUDO_ASKPASS=${old_value} with "${self_path}"`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
content = content.replace(/^export SUDO_ASKPASS=.*$/gm, `# sido\n${line}`)
|
|
104
256
|
} else {
|
|
105
|
-
content += `\n# sido\
|
|
257
|
+
content += `\n# sido\n${line}\n`
|
|
106
258
|
}
|
|
107
259
|
writeFileSync(path, content)
|
|
108
260
|
console.error(`[sido] installed to ${path}`)
|
|
@@ -115,8 +267,19 @@ function do_install(scope ) {
|
|
|
115
267
|
const r = spawnSync('cat', [path], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
116
268
|
content = r.stdout?.toString() ?? ''
|
|
117
269
|
}
|
|
118
|
-
|
|
119
|
-
|
|
270
|
+
const managed_pattern = /^# sido\nPath askpass .*$/gm
|
|
271
|
+
if (managed_pattern.test(content)) {
|
|
272
|
+
content = content.replace(managed_pattern, `# sido\n${line}`)
|
|
273
|
+
} else if (/^Path askpass /m.test(content)) {
|
|
274
|
+
const old_values = [...content.matchAll(/^Path askpass (.*)$/gm)].map(
|
|
275
|
+
(match) => match[1],
|
|
276
|
+
)
|
|
277
|
+
for (const old_value of old_values) {
|
|
278
|
+
console.error(
|
|
279
|
+
`[sido] warning: replacing Path askpass ${old_value} with "${self_path}"`,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
content = content.replace(/^Path askpass .*$/gm, `# sido\n${line}`)
|
|
120
283
|
} else {
|
|
121
284
|
content += `\n# sido\n${line}\n`
|
|
122
285
|
}
|
|
@@ -141,8 +304,7 @@ function do_uninstall(scope ) {
|
|
|
141
304
|
process.exit(1)
|
|
142
305
|
}
|
|
143
306
|
let content = readFileSync(path, 'utf-8')
|
|
144
|
-
content = content.replace(/^# sido\n
|
|
145
|
-
content = content.replace(/^export SUDO_ASKPASS=.*$/gm, '')
|
|
307
|
+
content = content.replace(/^# sido\nexport SUDO_ASKPASS=.*$\n?/gm, '')
|
|
146
308
|
content = content.replace(/\n{3,}/g, '\n\n').trim()
|
|
147
309
|
writeFileSync(path, content + '\n')
|
|
148
310
|
console.error(`[sido] removed from ${path}`)
|
|
@@ -207,32 +369,73 @@ function do_status(scope ) {
|
|
|
207
369
|
}
|
|
208
370
|
}
|
|
209
371
|
|
|
372
|
+
// Background `cat` drains the FIFO back to the parent while a blocking command
|
|
373
|
+
// (tmux popup / herdr pane run) fills it. Shell job control reaps the reader on
|
|
374
|
+
// cancel — see AGENTS.md: native fs open() on a FIFO cannot be cancelled from JS,
|
|
375
|
+
// but the shell naturally reaps the background cat on cancel.
|
|
376
|
+
function fifo_drain_script(
|
|
377
|
+
fifo_ref ,
|
|
378
|
+
block_cmd ,
|
|
379
|
+
status_var ,
|
|
380
|
+
) {
|
|
381
|
+
return [
|
|
382
|
+
`cat "${fifo_ref}" &`,
|
|
383
|
+
`reader=$!`,
|
|
384
|
+
block_cmd,
|
|
385
|
+
`${status_var}=$?`,
|
|
386
|
+
`if [ "$${status_var}" -ne 0 ]; then`,
|
|
387
|
+
` kill "$reader" 2>/dev/null`,
|
|
388
|
+
` wait "$reader" 2>/dev/null`,
|
|
389
|
+
` exit "$${status_var}"`,
|
|
390
|
+
`fi`,
|
|
391
|
+
`wait "$reader" 2>/dev/null`,
|
|
392
|
+
].join('\n')
|
|
393
|
+
}
|
|
394
|
+
|
|
210
395
|
// ── tmux ────────────────────────────────────────────────────────────────────
|
|
211
396
|
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// can't be cancelled mid-open (libuv threadpool blocking op), whereas the
|
|
216
|
-
// shell naturally reaps the background cat on cancel.
|
|
397
|
+
// tmux command-prompt cannot hide input (-N means numeric-only, not secret).
|
|
398
|
+
// Use a popup running bash read -s instead. The password still travels only
|
|
399
|
+
// through a FIFO in a private temporary directory.
|
|
217
400
|
|
|
218
401
|
function tmux_prompt() {
|
|
219
|
-
const
|
|
220
|
-
|
|
402
|
+
const check = spawnSync('tmux', ['display-message', '-p', '#S'], {
|
|
403
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
404
|
+
})
|
|
405
|
+
if (check.status !== 0) {
|
|
406
|
+
const detail = check.stderr?.toString().trim()
|
|
407
|
+
if (detail) console.error(detail)
|
|
408
|
+
console.error(
|
|
409
|
+
'[sido] tmux access denied; retry the sudo command with escalated permissions',
|
|
410
|
+
)
|
|
411
|
+
process.exit(1)
|
|
412
|
+
}
|
|
221
413
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
`
|
|
227
|
-
|
|
414
|
+
let resources
|
|
415
|
+
try {
|
|
416
|
+
resources = create_prompt_resources()
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error(`[sido] ${String(error)}`)
|
|
419
|
+
process.exit(1)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const block_cmd = [
|
|
423
|
+
`tmux display-popup -E -w 60% -h 5 \\`,
|
|
424
|
+
` -e "SIDO_ASKPASS=$2" -e "SIDO_PROMPT=$3" -e "SIDO_FIFO=$1" \\`,
|
|
425
|
+
` '"$SIDO_ASKPASS" _inner_prompt_receiver "$SIDO_PROMPT" "$SIDO_FIFO"'`,
|
|
228
426
|
].join('\n')
|
|
427
|
+
const script = fifo_drain_script('$1', block_cmd, 'popup_status')
|
|
229
428
|
|
|
230
|
-
|
|
231
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
232
|
-
})
|
|
429
|
+
let r
|
|
233
430
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
431
|
+
r = spawnSync(
|
|
432
|
+
'bash',
|
|
433
|
+
['-c', script, 'sido', resources.fifo, self_path, resources.prompt_file],
|
|
434
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
435
|
+
)
|
|
436
|
+
} finally {
|
|
437
|
+
remove_prompt_resources(resources)
|
|
438
|
+
}
|
|
236
439
|
process.stdout.write(read_stdout(r))
|
|
237
440
|
}
|
|
238
441
|
|
|
@@ -242,23 +445,6 @@ function tmux_prompt() {
|
|
|
242
445
|
// call and reaps the background cat on cancel.
|
|
243
446
|
|
|
244
447
|
function herdr_prompt() {
|
|
245
|
-
const fifo = tmp()
|
|
246
|
-
const prompt_file = tmp()
|
|
247
|
-
const script_file = tmp()
|
|
248
|
-
|
|
249
|
-
mkfifo(fifo)
|
|
250
|
-
writeFileSync(prompt_file, prompt)
|
|
251
|
-
writeFileSync(
|
|
252
|
-
script_file,
|
|
253
|
-
[
|
|
254
|
-
`#!/usr/bin/env bash`,
|
|
255
|
-
`prompt=$(cat "${prompt_file}")`,
|
|
256
|
-
`read -s -p "$prompt" pw`,
|
|
257
|
-
`echo "$pw" > "${fifo}"`,
|
|
258
|
-
].join('\n'),
|
|
259
|
-
)
|
|
260
|
-
chmodSync(script_file, 0o755)
|
|
261
|
-
|
|
262
448
|
let direction = 'right'
|
|
263
449
|
try {
|
|
264
450
|
const layout = spawnSync('herdr', ['pane', 'layout', '--current'], {
|
|
@@ -284,30 +470,41 @@ function herdr_prompt() {
|
|
|
284
470
|
?.pane_id
|
|
285
471
|
if (!pane_id) return void fallback_herdr()
|
|
286
472
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
473
|
+
let resources
|
|
474
|
+
try {
|
|
475
|
+
resources = create_prompt_resources()
|
|
476
|
+
} catch (error) {
|
|
477
|
+
spawnSync('herdr', ['pane', 'close', pane_id], {
|
|
478
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
479
|
+
})
|
|
480
|
+
console.error(`[sido] ${String(error)}`)
|
|
481
|
+
process.exit(1)
|
|
482
|
+
}
|
|
293
483
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
})
|
|
484
|
+
const block_cmd = `herdr pane run "$1" "$3" _inner_prompt_receiver "$4" "$2"`
|
|
485
|
+
const runner_script = fifo_drain_script('$2', block_cmd, 'run_status')
|
|
297
486
|
|
|
487
|
+
let r
|
|
298
488
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
489
|
+
r = spawnSync(
|
|
490
|
+
'bash',
|
|
491
|
+
[
|
|
492
|
+
'-c',
|
|
493
|
+
runner_script,
|
|
494
|
+
'sido',
|
|
495
|
+
pane_id,
|
|
496
|
+
resources.fifo,
|
|
497
|
+
self_path,
|
|
498
|
+
resources.prompt_file,
|
|
499
|
+
],
|
|
500
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
501
|
+
)
|
|
502
|
+
} finally {
|
|
503
|
+
remove_prompt_resources(resources)
|
|
504
|
+
spawnSync('herdr', ['pane', 'close', pane_id], {
|
|
505
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
506
|
+
})
|
|
507
|
+
}
|
|
311
508
|
process.stdout.write(read_stdout(r))
|
|
312
509
|
}
|
|
313
510
|
|
|
@@ -320,30 +517,34 @@ function fallback_herdr() {
|
|
|
320
517
|
// ── GUI (macOS / Linux) ────────────────────────────────────────────────────
|
|
321
518
|
|
|
322
519
|
function gui_prompt() {
|
|
323
|
-
if (
|
|
520
|
+
if (is_mac) return void mac_gui()
|
|
324
521
|
linux_gui()
|
|
325
522
|
}
|
|
326
523
|
|
|
327
524
|
function mac_gui() {
|
|
328
|
-
const safe_prompt = prompt.replace(/"/g, '\\"')
|
|
329
525
|
const r = spawnSync(
|
|
330
526
|
'osascript',
|
|
331
527
|
[
|
|
332
528
|
'-e',
|
|
333
|
-
|
|
529
|
+
'on run argv',
|
|
530
|
+
'-e',
|
|
531
|
+
'display dialog (item 1 of argv) with hidden answer default answer ""',
|
|
334
532
|
'-e',
|
|
335
533
|
'text returned of result',
|
|
534
|
+
'-e',
|
|
535
|
+
'end run',
|
|
536
|
+
display_prompt,
|
|
336
537
|
],
|
|
337
538
|
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
338
539
|
)
|
|
339
540
|
if (r.status !== 0) process.exit(1)
|
|
340
|
-
process.stdout.write(r.stdout?.toString().
|
|
541
|
+
process.stdout.write(r.stdout?.toString().replace(/\r?\n$/, '') ?? '')
|
|
341
542
|
}
|
|
342
543
|
|
|
343
544
|
function linux_gui() {
|
|
344
545
|
for (const prog of [
|
|
345
|
-
['zenity', '--password', '--title',
|
|
346
|
-
['kdialog', '--password',
|
|
546
|
+
['zenity', '--password', '--title', display_prompt],
|
|
547
|
+
['kdialog', '--password', display_prompt],
|
|
347
548
|
]) {
|
|
348
549
|
const [cmd, ...args] = prog
|
|
349
550
|
const r = spawnSync(cmd, args, { stdio: ['inherit', 'pipe', 'inherit'] })
|
|
@@ -359,13 +560,7 @@ function linux_gui() {
|
|
|
359
560
|
// ── TTY fallback ───────────────────────────────────────────────────────────
|
|
360
561
|
|
|
361
562
|
function tty_prompt() {
|
|
362
|
-
|
|
363
|
-
const r = spawnSync(
|
|
364
|
-
'bash',
|
|
365
|
-
['-c', `read -s -p "${safe_prompt}" pw && echo "$pw"`],
|
|
366
|
-
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
367
|
-
)
|
|
368
|
-
process.stdout.write(read_stdout(r))
|
|
563
|
+
process.stdout.write(read_secret(display_prompt))
|
|
369
564
|
}
|
|
370
565
|
|
|
371
566
|
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sido-askpass",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SUDO_ASKPASS shim for headless agent environments (tmux, Herdr, GUI, TTY)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/build.ts",
|
|
41
|
+
"e2e": "node e2e/run.ts",
|
|
41
42
|
"typecheck": "tsc",
|
|
42
43
|
"format": "prettier --write .",
|
|
43
44
|
"format:check": "prettier --check ."
|