opencode-polkit 0.3.0 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-11-04
9
+
10
+ ### Added
11
+
12
+ - `src/shims.ts`: `su` stand-in. `su -c COMMAND [USER]` (also `-cCOMMAND`,
13
+ `--command COMMAND`, `--command=COMMAND`) becomes
14
+ `pkexec --keep-cwd --user USER sh -c COMMAND`, because a non-login `su` keeps
15
+ the working directory and runs its command through a shell. Every other form
16
+ is refused with status `64` and one line naming the alternative: `su`,
17
+ `su -`, `su -l` (login shell → `pkexec bash`), `-m`/`-p`/`-w` (pkexec
18
+ sanitizes the environment), `-s` (name the shell in the command), `-g`/`-G`
19
+ (pkexec cannot switch groups), `--pty` (no terminal), `-f` and
20
+ `--session-command`.
21
+ - README scope statement: the plugin covers elevation → polkit only; ssh,
22
+ gpg/pinentry, keyrings, database clients, `kinit` and disk encryption are
23
+ explicitly out of scope and need an askpass helper or a PTY.
24
+
25
+ ### Changed
26
+
27
+ - README authorisation note: a desktop agent may reuse the password it already
28
+ holds, so a second command can run without a dialog; that cache belongs to
29
+ the agent and must not be relied on. `su -c … USER` authenticates an
30
+ administrator rather than `USER`, and runs the command with `sh -c`.
31
+
8
32
  ## [0.3.0] - 2026-11-01
9
33
 
10
34
  0.3.0 replaces how the plugin decides anything. It used to parse the command
package/README.md CHANGED
@@ -1,9 +1,23 @@
1
1
  # opencode-polkit
2
2
 
3
- Redirects `sudo` and `doas` to `pkexec` inside OpenCode, so authorisation goes
4
- through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal the
5
- shell tool does not have. Applies to commands run by the shell tool; `sudo` in
6
- other terminals is unchanged.
3
+ Redirects `sudo`, `doas` and `su -c` to `pkexec` inside OpenCode, so
4
+ authorisation goes through the desktop's polkit agent (KDE, GNOME, …) instead of
5
+ a terminal the shell tool does not have. Applies to commands run by the shell
6
+ tool; `sudo` in other terminals is unchanged.
7
+
8
+ ## Scope
9
+
10
+ This plugin covers one category: **elevation → polkit**. It exists because the
11
+ shell tool has no terminal, so any program that reads a password from `/dev/tty`
12
+ fails there. `sudo`, `doas` and `su -c` are that category, and they are routed
13
+ to `pkexec`, which asks the desktop's polkit agent instead.
14
+
15
+ Password prompts that are not polkit's business are out of scope: `ssh`,
16
+ `ssh-add`, `gpg`/pinentry, keyrings (`secret-tool`, KWallet), database clients
17
+ (`psql`, `mysql -p`, `redis-cli`), `kinit`, and disk encryption (`cryptsetup`,
18
+ `fscrypt`, `gocryptfs`). They read the process's own terminal, so they need an
19
+ askpass helper or a pseudo-terminal, not polkit; mixing them in would make this
20
+ plugin's boundary meaningless.
7
21
 
8
22
  ## Installation
9
23
 
@@ -37,16 +51,20 @@ For a local checkout, run `npm run build` once, then reference the directory:
37
51
  | `sudo -n …` | refused: pkexec has no non-interactive mode |
38
52
  | `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
39
53
  | `sudoedit`, `visudo` | refused, with the alternative |
54
+ | `su -c 'id'` | dialog, then runs as root |
55
+ | `su -c 'id' bob` | as above, as `bob` (administrator password) |
56
+ | `su`, `su -`, `su -l` | refused: no interactive shell, no terminal |
57
+ | `su -m`, `-s`, `-g`, `-w`, `--pty` | refused, with the alternative |
40
58
  | `pkexec …` | passed through |
41
59
 
42
60
  Refused invocations exit with status 64 and one line naming the alternative.
43
61
 
44
62
  ## How it works
45
63
 
46
- `sudo` and `doas` stand-ins are placed first on `PATH`, so the shell resolves
47
- those instead of the system binaries, and they forward the command to `pkexec`.
48
- Command text is never rewritten, so quoting, pipes and subshells need no special
49
- handling.
64
+ `sudo`, `doas` and `su` stand-ins are placed first on `PATH`, so the shell
65
+ resolves those instead of the system binaries, and they forward the command to
66
+ `pkexec`. Command text is never rewritten, so quoting, pipes and subshells need
67
+ no special handling.
50
68
 
51
69
  ## Authorisation
52
70
 
@@ -54,11 +72,21 @@ handling.
54
72
  one; other actions may not, according to policy. The plugin takes no part in
55
73
  that decision.
56
74
 
75
+ `org.freedesktop.policykit.exec` asks for administrator authentication on every
76
+ call, but the desktop agent may keep the password it just received and answer
77
+ the next challenge itself (KDE's agent does). A second command in the same
78
+ session then runs without a dialog. That cache belongs to the agent: another
79
+ agent, a restarted agent or a dismissed dialog means the next command prompts
80
+ again, so do not rely on it.
81
+
57
82
  ## Limitations
58
83
 
59
84
  - Absolute paths (`/usr/bin/sudo`) and relative ones (`./sudo`) bypass `PATH`.
60
85
  - Commands that reset `PATH` (`env -i`, `PATH=/usr/bin …`) bypass it as well.
61
86
  - Programs that invoke `sudo` with a scrubbed environment bypass it.
87
+ - `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
88
+ account that real `su` would allow can be refused here.
89
+ - `su -c` runs the command with `sh -c`, not with the target user's login shell.
62
90
 
63
91
  These bypass the redirect, not polkit, which still decides whether the command
64
92
  runs.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { installPolkitShims, removePolkitShims } from "./shims.js";
5
5
  */
6
6
  const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
7
7
  /**
8
- * Redirects sudo/doas to pkexec without touching the command text.
8
+ * Redirects sudo/doas/su -c to pkexec without touching the command text.
9
9
  *
10
10
  * The shell tool spawns `$SHELL -c 'command'` with the environment built here,
11
11
  * so putting the shims first on PATH lets the shell's own name resolution pick
package/dist/shims.d.ts CHANGED
@@ -8,8 +8,8 @@ export declare const SHIM_USAGE_EXIT = 64;
8
8
  /**
9
9
  * Writes the shim scripts into a fresh private directory and returns it.
10
10
  * The caller prepends this directory to PATH; the shell then resolves
11
- * `sudo`/`doas` to these files instead of the system ones. The directory is
12
- * 0700 so no other local user can rewrite the scripts.
11
+ * `sudo`/`doas`/`su` to these files instead of the system ones. The directory
12
+ * is 0700 so no other local user can rewrite the scripts.
13
13
  */
14
14
  export declare function installPolkitShims(): string;
15
15
  /** Deletes a directory created by installPolkitShims. */
package/dist/shims.js CHANGED
@@ -170,6 +170,112 @@ if [ -n "$user" ]; then
170
170
  fi
171
171
  run_pkexec "$@"
172
172
  `;
173
+ /**
174
+ * The su shim. `su -c` is the only form pkexec can express exactly: a command
175
+ * run as the target user through a shell, in the current directory. Every
176
+ * other form asks for a login shell, an environment, a shell binary, a group
177
+ * or a terminal, none of which pkexec can honour, so it is refused instead.
178
+ */
179
+ const SU_SHIM = `#!/bin/sh
180
+ # opencode-polkit: su shim. Routes su -c to pkexec so polkit can authorize it.
181
+ # Generated at plugin startup; do not edit.
182
+
183
+ fail() {
184
+ printf 'opencode-polkit: %s\\n' "$1" >&2
185
+ exit ${SHIM_USAGE_EXIT}
186
+ }
187
+
188
+ command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
189
+
190
+ user=
191
+ cmd=
192
+ has_cmd=
193
+ while [ $# -gt 0 ]; do
194
+ case $1 in
195
+ --)
196
+ shift
197
+ break
198
+ ;;
199
+ -c)
200
+ [ $# -ge 2 ] || fail 'su -c needs a command: su -c COMMAND [USER]'
201
+ cmd=$2
202
+ has_cmd=1
203
+ shift 2
204
+ ;;
205
+ -c?*)
206
+ cmd=\${1#-c}
207
+ has_cmd=1
208
+ shift
209
+ ;;
210
+ --command)
211
+ [ $# -ge 2 ] || fail 'su --command needs a command: su --command COMMAND [USER]'
212
+ cmd=$2
213
+ has_cmd=1
214
+ shift 2
215
+ ;;
216
+ --command=*)
217
+ cmd=\${1#--command=}
218
+ has_cmd=1
219
+ shift
220
+ ;;
221
+ - | -l | --login)
222
+ fail 'su starts a login shell; run pkexec bash instead.'
223
+ ;;
224
+ -s | --shell | -s?* | --shell=*)
225
+ fail 'su -s picks the shell; name the shell in the command: pkexec sh -c ...'
226
+ ;;
227
+ -m | -p | --preserve-environment)
228
+ fail 'pkexec deliberately sanitizes the environment; drop -m/-p.'
229
+ ;;
230
+ -g | -G | --group | --supp-group | -g?* | -G?* | --group=* | --supp-group=*)
231
+ fail 'su -g/-G switches groups; pkexec can only switch users.'
232
+ ;;
233
+ -w | --whitelist-environment | -w?* | --whitelist-environment=*)
234
+ fail 'su -w whitelists environment variables; pkexec sanitizes the environment.'
235
+ ;;
236
+ -P | --pty)
237
+ fail 'su --pty asks for a terminal; the shell tool has none.'
238
+ ;;
239
+ -f | --fast)
240
+ fail 'su -f is a shell flag; pass it to the shell in the command instead.'
241
+ ;;
242
+ --session-command | --session-command=*)
243
+ fail 'su --session-command keeps the session; pkexec always starts a new process.'
244
+ ;;
245
+ -h | --help | -V | --version)
246
+ fail "option has no pkexec equivalent: $1"
247
+ ;;
248
+ -?*)
249
+ fail "unknown su option: $1"
250
+ ;;
251
+ *)
252
+ [ -z "$user" ] || fail 'su passes arguments after the user name to the shell; use su -c COMMAND USER.'
253
+ user=$1
254
+ shift
255
+ ;;
256
+ esac
257
+ done
258
+
259
+ # Operands after -- are the same user-then-nothing sequence.
260
+ while [ $# -gt 0 ]; do
261
+ [ -z "$user" ] || fail 'su passes arguments after the user name to the shell; use su -c COMMAND USER.'
262
+ user=$1
263
+ shift
264
+ done
265
+
266
+ [ -n "$user" ] || user=root
267
+ [ -n "$has_cmd" ] || fail 'su without -c opens an interactive shell; run pkexec bash instead.'
268
+
269
+ # su keeps the working directory unless it is a login shell, and --keep-cwd is
270
+ # how pkexec does the same. pkexec is silent when the dialog is dismissed
271
+ # (status 126), so say what the status means.
272
+ pkexec --keep-cwd --user "$user" sh -c "$cmd"
273
+ status=$?
274
+ if [ $status -eq 126 ]; then
275
+ printf 'opencode-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
276
+ fi
277
+ exit $status
278
+ `;
173
279
  /** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
174
280
  const SUDOEDIT_SHIM = `#!/bin/sh
175
281
  printf 'opencode-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
@@ -184,14 +290,15 @@ exit ${SHIM_USAGE_EXIT}
184
290
  const POLKIT_SHIM_SCRIPTS = {
185
291
  sudo: SUDO_SHIM,
186
292
  doas: DOAS_SHIM,
293
+ su: SU_SHIM,
187
294
  sudoedit: SUDOEDIT_SHIM,
188
295
  visudo: VISUDO_SHIM,
189
296
  };
190
297
  /**
191
298
  * Writes the shim scripts into a fresh private directory and returns it.
192
299
  * The caller prepends this directory to PATH; the shell then resolves
193
- * `sudo`/`doas` to these files instead of the system ones. The directory is
194
- * 0700 so no other local user can rewrite the scripts.
300
+ * `sudo`/`doas`/`su` to these files instead of the system ones. The directory
301
+ * is 0700 so no other local user can rewrite the scripts.
195
302
  */
196
303
  export function installPolkitShims() {
197
304
  const directory = mkdtempSync(join(shimBaseDirectory(), "opencode-polkit-"));
package/dist/shims.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"shims.js","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFtB,CAAA;AAED,yDAAyD;AACzD,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDtB,CAAA;AAED,8EAA8E;AAC9E,MAAM,aAAa,GAAG;;OAEf,eAAe;CACrB,CAAA;AAED,qFAAqF;AACrF,MAAM,WAAW,GAAG;;OAEb,eAAe;CACrB,CAAA;AAED,+EAA+E;AAC/E,MAAM,mBAAmB,GAA2B;IAClD,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;CACpB,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAC5E,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;QAClC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC3B,mEAAmE;QACnE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACxB,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;IAC3C,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;AACnF,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACrD,CAAC"}
1
+ {"version":3,"file":"shims.js","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFtB,CAAA;AAED,yDAAyD;AACzD,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDtB,CAAA;AAED;;;;;GAKG;AACH,MAAM,OAAO,GAAG;;;;;;QAMR,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6FtB,CAAA;AAED,8EAA8E;AAC9E,MAAM,aAAa,GAAG;;OAEf,eAAe;CACrB,CAAA;AAED,qFAAqF;AACrF,MAAM,WAAW,GAAG;;OAEb,eAAe;CACrB,CAAA;AAED,+EAA+E;AAC/E,MAAM,mBAAmB,GAA2B;IAClD,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,EAAE,EAAE,OAAO;IACX,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;CACpB,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAC5E,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;QAClC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC3B,mEAAmE;QACnE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACxB,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;IAC3C,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;AACnF,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACrD,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-polkit",
3
- "version": "0.3.0",
4
- "description": "Redirect sudo/doas to pkexec in OpenCode via PATH shims, so the system polkit agent handles authentication",
3
+ "version": "0.3.1",
4
+ "description": "Redirect sudo, doas and su to pkexec in OpenCode via PATH shims, so the system polkit agent handles authentication",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^24.0.0",
39
39
  "typescript": "^5.5.0"
40
40
  },
41
- "keywords": ["opencode-plugin", "sudo", "doas", "pkexec", "polkit", "security"],
41
+ "keywords": ["opencode-plugin", "sudo", "doas", "su", "pkexec", "polkit", "security"],
42
42
  "license": "MIT",
43
43
  "repository": {
44
44
  "type": "git",