pi-polkit 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,53 @@ 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.2.0] - 2026-11-05
9
+
10
+ ### Added
11
+
12
+ - `sudo -n`, `doas -n`: a non-interactive `pkcheck` on
13
+ `org.freedesktop.policykit.exec` decides whether the command runs. polkit grants
14
+ it without asking, or the shim exits `1` with `sudo: a password is required` and
15
+ the reason that dropping `-n` lets the agent answer. Measured: a successful
16
+ `pkexec` leaves no temporary authorization behind (`pkcheck --list-temp` stays
17
+ empty), so the silent second run is the agent's own password cache and no
18
+ non-interactive query can predict it; `-n` therefore reports policy, not a
19
+ guess.
20
+ - `sudo -v` performs the authentication (`pkcheck --allow-user-interaction`) and
21
+ exits `0`, `1`, `126` when the dialog is dismissed, or `127`; `sudo -nv` uses
22
+ the non-interactive check.
23
+ - `sudo -l` prints `pkaction --verbose` for the action and this session's
24
+ temporary authorizations.
25
+ - `sudo -k`/`-K` revoke polkit's temporary authorizations and say that an agent's
26
+ own password cache cannot be cleared from here.
27
+ - `sudo -H`, `-N`, `-P` are accepted as no-ops: pkexec already sets `HOME`, keeps
28
+ no timestamp and leaves groups alone.
29
+ - `sudo -s COMMAND` and `--shell` run the command through a shell, `sudo -b`
30
+ starts it in the background and returns, and `sudo -E`/
31
+ `--preserve-env[=LIST]` re-export the environment inside the command.
32
+ - `sudo --help` and `sudo -V` are answered by the real binary, so scripts that
33
+ probe sudo keep working.
34
+ - Short options bundle (`sudo -nu bob`), and unknown long options report their own
35
+ name instead of `--`.
36
+ - Registered bash tool: one guideline line tells the model that elevation goes
37
+ through polkit, what `-n`, `-v` and `-l` answer, so it plans with the real
38
+ contract instead of learning it from a failed call.
39
+ - `pi-polkit-env`: helper that turns `NAME=VALUE` arguments into export
40
+ statements. `-E` passes entries through argv (`env -0` into `xargs -0`), so
41
+ spaces, quotes and newlines survive, and it drops every shim directory from
42
+ `PATH` so an elevated command cannot resolve sudo back to a shim.
43
+
44
+ ### Changed
45
+
46
+ - The shims share one prologue: `fail`, the polkit subject (pinned with
47
+ `pid,start-time,uid` as `pkcheck(1)` requires), the two checks and the `pkexec`
48
+ wrapper.
49
+ - `-n` failures exit `1` like sudo, not `64`: `64` now means only that the
50
+ invocation cannot be expressed. README documents the four-way handling
51
+ (translated, no-op, checked, refused) and polkit's missing temp authorization.
52
+ - `sudo -i`, `-S`, `-A`, `sudoedit` and `visudo` still refuse, now from the shared
53
+ prelude.
54
+
8
55
  ## [0.1.1] - 2026-11-04
9
56
 
10
57
  ### Added
package/README.md CHANGED
@@ -46,8 +46,16 @@ pi -e ./pi-polkit
46
46
  | `sudo apt install x` | polkit dialog, then runs as root |
47
47
  | `sudo -u bob …` | as above, as `bob` |
48
48
  | `cat x \| sudo tee y` | dialog for `tee` |
49
- | `sudo -n …` | refused: pkexec has no non-interactive mode |
50
- | `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
49
+ | `sudo -n …` | runs only if polkit would authorise without asking; otherwise exit 1 with `sudo: a password is required` |
50
+ | `sudo -v` | performs the authentication, as `sudo -v` does |
51
+ | `sudo -l` | prints the polkit policy and this session's temporary authorizations |
52
+ | `sudo -k`, `-K` | revokes polkit's temporary authorizations |
53
+ | `sudo -H`, `-N`, `-P` | accepted: pkexec already does what they ask |
54
+ | `sudo -s CMD` | runs `CMD` through a shell, as root |
55
+ | `sudo -E …` | runs with the caller's environment |
56
+ | `sudo -b …` | starts the command in the background |
57
+ | `sudo --help`, `-V` | answered by the real sudo; nothing elevates |
58
+ | `sudo -i`, `-S`, `-A` | refused, with the equivalent command |
51
59
  | `sudoedit`, `visudo` | refused, with the alternative |
52
60
  | `su -c 'id'` | dialog, then runs as root |
53
61
  | `su -c 'id' bob` | as above, as `bob` (administrator password) |
@@ -55,7 +63,9 @@ pi -e ./pi-polkit
55
63
  | `su -m`, `-s`, `-g`, `-w`, `--pty` | refused, with the alternative |
56
64
  | `pkexec …` | passed through |
57
65
 
58
- Refused invocations exit with status 64 and one line naming the alternative.
66
+ An option is refused only when polkit cannot express it: it needs a terminal, a
67
+ password channel polkit does not have, or an editor. Refusals exit with status 64
68
+ and one line naming the alternative.
59
69
 
60
70
  ## How it works
61
71
 
@@ -73,7 +83,9 @@ Two wiring paths put the stand-ins on `PATH`, one per execution form:
73
83
  line instead.
74
84
 
75
85
  The shim directory is created lazily in `$XDG_RUNTIME_DIR` (falling back to
76
- `/tmp`) as `0700` and removed at session shutdown.
86
+ `/tmp`) as `0700` and removed at session shutdown. It also holds
87
+ `pi-polkit-env`, the helper that `sudo -E` uses to re-export an environment
88
+ pkexec stripped.
77
89
 
78
90
  ## Authorisation
79
91
 
@@ -88,6 +100,14 @@ session then runs without a dialog. That cache belongs to the agent: another
88
100
  agent, a restarted agent or a dismissed dialog means the next command prompts
89
101
  again, so do not rely on it.
90
102
 
103
+ Measured on polkit 128 with `polkit-gnome`'s agent: after a successful `pkexec`,
104
+ `pkcheck --list-temp` prints nothing and a non-interactive `pkcheck` still
105
+ reports that authentication is required. polkit records no temporary
106
+ authorization for this action, so the silent second run is the agent answering
107
+ from its own copy of the password, and no user-space query can predict it. That
108
+ is why `sudo -n` reports what the policy says rather than guessing whether a
109
+ dialog would appear, and why `sudo -k` can only clear polkit's own record.
110
+
91
111
  ## Limitations
92
112
 
93
113
  - Absolute paths (`/usr/bin/sudo`) and relative ones (`./sudo`) bypass `PATH`.
@@ -96,6 +116,11 @@ again, so do not rely on it.
96
116
  - `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
97
117
  account that real `su` would allow can be refused here.
98
118
  - `su -c` runs the command with `sh -c`, not with the target user's login shell.
119
+ - `sudo -E` re-exports the environment through `pkexec`'s argument list, so the
120
+ values are visible in `ps` while the command starts. Exported shell functions
121
+ are skipped, and `PATH` loses the shim directories.
122
+ - `sudo -k` cannot clear the password a desktop agent holds; only polkit's
123
+ temporary authorizations are revoked.
99
124
  - PowerShell is not handled; the extension targets Linux only.
100
125
 
101
126
  These bypass the redirect, not polkit, which still decides whether the command
@@ -108,11 +133,12 @@ without one, `pkexec` fails instead of prompting.
108
133
 
109
134
  ## Exit status
110
135
 
111
- | Status | Meaning |
112
- |--------|----------------------------------------------|
113
- | 126 | the polkit dialog was dismissed |
114
- | 127 | not authorized, or the program could not run |
115
- | 64 | the extension refused the invocation |
136
+ | Status | Meaning |
137
+ |--------|--------------------------------------------------------|
138
+ | 1 | `-n` or `-v` could not be satisfied: polkit requires authentication, or refuses |
139
+ | 126 | the polkit dialog was dismissed |
140
+ | 127 | not authorized, or the program could not run |
141
+ | 64 | the extension refused the invocation |
116
142
 
117
143
  Messages are in English.
118
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-polkit",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Redirect sudo, doas and su to pkexec in Pi via PATH shims, so the desktop polkit agent handles authentication",
5
5
  "type": "module",
6
6
  "pi": {
@@ -19,11 +19,15 @@ export default function polkitRedirect(pi: ExtensionAPI): void {
19
19
  // The bash tool spawns with a prepared environment, so the shims reach it as
20
20
  // a PATH prepend. Registering the built-in's own definition under its own
21
21
  // name replaces it and keeps its prompt metadata and renderers.
22
- pi.registerTool(
23
- createBashToolDefinition(process.cwd(), {
24
- spawnHook: ({ command, cwd, env }) => ({ command, cwd, env: withPolkitShimsOnPath(env) }),
25
- }),
22
+ const bash = createBashToolDefinition(process.cwd(), {
23
+ spawnHook: ({ command, cwd, env }) => ({ command, cwd, env: withPolkitShimsOnPath(env) }),
24
+ })
25
+ // The model plans privilege with what it knows about sudo. Say once, in the
26
+ // system prompt, how this shell differs, so it does not learn it by failing.
27
+ bash.promptGuidelines?.push(
28
+ "`sudo`, `doas` and `su -c` are routed to pkexec, so a desktop polkit dialog authorises them instead of a terminal password prompt. `sudo -n` runs the command only if polkit would authorise it without asking, `sudo -v` performs the authentication, and `sudo -l` prints the polkit policy that decides.",
26
29
  )
30
+ pi.registerTool(bash)
27
31
 
28
32
  pi.on("user_bash", () => ({ operations: polkitShimsUserBashOperations() }))
29
33
 
package/polkit-shims.ts CHANGED
@@ -10,103 +10,412 @@ import { isAbsolute, join } from "node:path"
10
10
  */
11
11
  export const SHIM_USAGE_EXIT = 64
12
12
 
13
+ /** The action pkexec checks; the checks below ask polkit about the same one. */
14
+ const POLKIT_ACTION = "org.freedesktop.policykit.exec"
15
+
13
16
  /**
14
- * The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not
15
- * this extension — decides that `sudo` means this file. It forwards to pkexec
16
- * and never parses what pkexec prints: the exit status is the whole result.
17
- *
18
- * Only options with an exact pkexec equivalent are translated. Everything
19
- * else is refused with an actionable message rather than forwarded, because
20
- * pkexec would reject it with a message that does not say what to do instead.
17
+ * The part every shim starts with: one refusal helper, and a check that the two
18
+ * polkit tools exist at all.
21
19
  */
22
- const SUDO_SHIM = `#!/bin/sh
23
- # pi-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
24
- # Generated at extension startup; do not edit.
25
-
26
- fail() {
20
+ const SHIM_PRELUDE = `fail() {
27
21
  printf 'pi-polkit: %s\\n' "$1" >&2
28
22
  exit ${SHIM_USAGE_EXIT}
29
23
  }
30
24
 
31
25
  command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
26
+ command -v pkcheck >/dev/null 2>&1 || fail 'pkcheck is not installed; install polkit'
27
+ `
28
+
29
+ /**
30
+ * The checks that answer what sudo's timestamp file used to answer, plus the
31
+ * pkexec wrapper. Only exit statuses are read: pkcheck's messages and its
32
+ * `polkit.result` detail are localised and addressed to humans, so parsing them
33
+ * would break on the next locale or polkit release.
34
+ */
35
+ const SHIM_CHECKS = `# The action pkexec checks: a pkcheck verdict on it describes what the next
36
+ # pkexec will do, which is how the shims answer what sudo's timestamp answered.
37
+ ACTION=${POLKIT_ACTION}
38
+
39
+ # pkcheck(1) warns that a bare pid races with pid reuse, so pin the start time.
40
+ subject() {
41
+ started=$(awk '{ print $22 }' /proc/$$/stat 2>/dev/null)
42
+ if [ -n "$started" ]; then
43
+ printf '%s,%s,%s' "$$" "$started" "$(id -u)"
44
+ else
45
+ printf '%s' "$$"
46
+ fi
47
+ }
48
+
49
+ # 0 grants the action without asking anyone, 2 requires authentication,
50
+ # 1 refuses it. pkcheck's own 126 (usage) and 127 (error) pass through.
51
+ check_without_prompt() {
52
+ pkcheck --action-id "$ACTION" --process "$(subject)" >/dev/null 2>&1
53
+ }
54
+
55
+ # The same check with the desktop agent allowed to answer. An agent that still
56
+ # holds the password answers without a dialog, which is why a 0 here also means
57
+ # the next pkexec runs as root without asking.
58
+ check_with_prompt() {
59
+ pkcheck --action-id "$ACTION" --process "$(subject)" --allow-user-interaction >/dev/null 2>&1
60
+ }
61
+
62
+ # -n means "run only if no one has to be asked". polkit cannot say whether the
63
+ # agent would answer from its cache, so a 2 is reported as needing a password
64
+ # and the command is not run: that is what -n promises, in both directions.
65
+ require_without_prompt() {
66
+ if [ -n "$noninteractive" ]; then
67
+ check_without_prompt
68
+ status=$?
69
+ case $status in
70
+ 0)
71
+ return 0
72
+ ;;
73
+ 1)
74
+ printf 'pi-polkit: %s: polkit denies %s to this process\\n' "$PROG" "$ACTION" >&2
75
+ exit 1
76
+ ;;
77
+ 2)
78
+ printf '%s: a password is required (polkit requires authentication for %s; drop -n to let the desktop agent answer, which it may do from its cache)\\n' "$PROG" "$ACTION" >&2
79
+ exit 1
80
+ ;;
81
+ *)
82
+ printf 'pi-polkit: %s: polkit check failed (status %s)\\n' "$PROG" "$status" >&2
83
+ exit 127
84
+ ;;
85
+ esac
86
+ fi
87
+ return 0
88
+ }
89
+
90
+ # pkexec is silent when the dialog is dismissed (status 126), so say what the
91
+ # status means. Every other failure path already prints its own reason.
92
+ run_pkexec() {
93
+ pkexec "$@"
94
+ status=$?
95
+ if [ $status -eq 126 ]; then
96
+ printf 'pi-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
97
+ fi
98
+ exit $status
99
+ }
100
+ `
101
+
102
+ /**
103
+ * The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not this
104
+ * extension — decides that `sudo` means this file. It forwards to pkexec and
105
+ * never parses what pkexec prints: the exit status is the whole result.
106
+ *
107
+ * Options are handled in four ways, and only the last one refuses. Those whose
108
+ * effect pkexec already has (`-H`, `-N`, `-P`) are accepted as no-ops; those
109
+ * polkit answers better than a run (`-n`, `-v`, `-l`, `-k`) become checks; those
110
+ * a wrapper can express (`-s COMMAND`, `-E`, `-b`) are translated; the rest,
111
+ * which need a terminal or a password channel polkit does not have, are refused
112
+ * with one line naming the alternative.
113
+ */
114
+ const SUDO_SHIM = `#!/bin/sh
115
+ # pi-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
116
+ # Generated at extension startup; do not edit.
32
117
 
118
+ ${SHIM_PRELUDE}
119
+ ${SHIM_CHECKS}
120
+ PROG=sudo
33
121
  user=
122
+ noninteractive=
123
+ preserve=
124
+ preserve_names=
125
+ background=
126
+ mode=
127
+
34
128
  while [ $# -gt 0 ]; do
35
129
  case $1 in
36
130
  --)
37
131
  shift
38
132
  break
39
133
  ;;
40
- -u)
41
- [ $# -ge 2 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
42
- user=$2
43
- shift 2
44
- ;;
45
134
  --user)
46
135
  [ $# -ge 2 ] || fail 'sudo --user needs a username: --user USER PROGRAM'
47
136
  user=$2
48
137
  shift 2
49
138
  ;;
50
- -u?*)
51
- user=\${1#-u}
52
- [ -n "$user" ] || fail 'sudo -u needs a username: -u USER PROGRAM'
53
- shift
54
- ;;
55
139
  --user=*)
56
140
  user=\${1#--user=}
57
- [ -n "$user" ] || fail 'sudo --user needs a username: --user USER PROGRAM'
58
141
  shift
59
142
  ;;
60
- -n | --non-interactive)
61
- fail 'sudo -n is not supported: pkexec has no non-interactive mode, polkit decides whether authentication is needed. Drop -n.'
143
+ --non-interactive)
144
+ noninteractive=1
145
+ shift
146
+ ;;
147
+ --preserve-env)
148
+ preserve=1
149
+ shift
150
+ ;;
151
+ --preserve-env=*)
152
+ preserve=1
153
+ preserve_names=\${1#--preserve-env=}
154
+ shift
62
155
  ;;
63
- -v | --validate | -k | --reset-timestamp | -K | --remove-timestamp)
64
- fail 'sudo credential caching does not exist under polkit; drop -v/-k.'
156
+ --set-home | --no-update | --preserve-groups)
157
+ shift
65
158
  ;;
66
- -l | --list)
67
- fail 'sudo -l is not available under polkit; inspect policy with pkaction --verbose.'
159
+ --list)
160
+ mode=list
161
+ shift
68
162
  ;;
69
- -e | --edit)
70
- fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
163
+ --validate)
164
+ mode=validate
165
+ shift
71
166
  ;;
72
- -i | --login | -s | --shell)
73
- fail 'sudo -i/-s opens a login shell; run pkexec bash instead.'
167
+ --reset-timestamp | --remove-timestamp)
168
+ mode=revoke
169
+ shift
74
170
  ;;
75
- -S | --stdin)
76
- fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
171
+ --background)
172
+ background=1
173
+ shift
77
174
  ;;
78
- -A | --askpass)
79
- fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
175
+ --help)
176
+ mode=help
177
+ shift
80
178
  ;;
81
- -b | --background)
82
- fail 'sudo -b backgrounds the command; append & instead.'
179
+ --version)
180
+ mode=version
181
+ shift
83
182
  ;;
84
- -E | --preserve-env | --preserve-env=* | -H | --set-home | -P | --preserve-groups | -N | --no-update)
85
- fail 'pkexec deliberately sanitizes the environment; drop -E/-H/-P.'
183
+ --stdin | --askpass | --edit | --login)
184
+ case $1 in
185
+ --edit)
186
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
187
+ ;;
188
+ --stdin)
189
+ fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
190
+ ;;
191
+ --askpass)
192
+ fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
193
+ ;;
194
+ *)
195
+ fail 'sudo -i opens a login shell; run pkexec bash instead.'
196
+ ;;
197
+ esac
198
+ ;;
199
+ --shell)
200
+ mode=shell
201
+ shift
86
202
  ;;
87
- -?*)
203
+ --*)
88
204
  fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
89
205
  ;;
206
+ -?*)
207
+ rest=\${1#-}
208
+ shift
209
+ while [ -n "$rest" ]; do
210
+ letter=\${rest%"\${rest#?}"}
211
+ rest=\${rest#?}
212
+ case $letter in
213
+ u)
214
+ if [ -n "$rest" ]; then
215
+ user=$rest
216
+ rest=
217
+ else
218
+ [ $# -ge 1 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
219
+ user=$1
220
+ shift
221
+ fi
222
+ ;;
223
+ n)
224
+ noninteractive=1
225
+ ;;
226
+ E)
227
+ preserve=1
228
+ ;;
229
+ H | N | P)
230
+ # pkexec already sets HOME, keeps no timestamp and leaves groups alone.
231
+ ;;
232
+ b)
233
+ background=1
234
+ ;;
235
+ l)
236
+ mode=list
237
+ ;;
238
+ v)
239
+ mode=validate
240
+ ;;
241
+ k | K)
242
+ mode=revoke
243
+ ;;
244
+ s)
245
+ mode=shell
246
+ ;;
247
+ h)
248
+ mode=help
249
+ ;;
250
+ V)
251
+ mode=version
252
+ ;;
253
+ i)
254
+ fail 'sudo -i opens a login shell; run pkexec bash instead.'
255
+ ;;
256
+ e)
257
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
258
+ ;;
259
+ S)
260
+ fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
261
+ ;;
262
+ A)
263
+ fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
264
+ ;;
265
+ *)
266
+ fail "option has no pkexec equivalent: -$letter (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
267
+ ;;
268
+ esac
269
+ done
270
+ ;;
90
271
  *)
91
272
  break
92
273
  ;;
93
274
  esac
94
275
  done
95
276
 
96
- # pkexec is silent when the dialog is dismissed (status 126), so say what the
97
- # status means. Every other failure path already prints its own reason.
98
- run_pkexec() {
99
- pkexec "$@"
277
+ # --help and --version describe the binary, not the authorisation, so nothing is
278
+ # elevated: answering them exactly keeps scripts that probe sudo working.
279
+ if [ "$mode" = help ] || [ "$mode" = version ]; then
280
+ for candidate in /usr/bin/sudo /bin/sudo /usr/local/bin/sudo; do
281
+ [ -x "$candidate" ] || continue
282
+ if [ "$mode" = help ]; then
283
+ exec "$candidate" --help
284
+ fi
285
+ exec "$candidate" --version
286
+ done
287
+ fail 'the real sudo is not installed, so --help and --version cannot be answered'
288
+ fi
289
+
290
+ # -l reports the policy polkit will apply and the authorizations this session
291
+ # already holds. Both halves are local and never prompt.
292
+ if [ "$mode" = list ]; then
293
+ pkaction --action-id "$ACTION" --verbose || exit 127
294
+ printf '\\npi-polkit: temporary authorizations in this session\\n'
295
+ pkcheck --list-temp || exit 127
296
+ exit 0
297
+ fi
298
+
299
+ # -v asks the same question sudo -v asks: can authentication succeed right now?
300
+ if [ "$mode" = validate ]; then
301
+ if [ -n "$noninteractive" ]; then
302
+ check_without_prompt
303
+ else
304
+ check_with_prompt
305
+ fi
100
306
  status=$?
101
- if [ $status -eq 126 ]; then
102
- printf 'pi-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
307
+ case $status in
308
+ 0)
309
+ exit 0
310
+ ;;
311
+ 1)
312
+ printf 'pi-polkit: sudo: polkit denies %s to this process\\n' "$ACTION" >&2
313
+ exit 1
314
+ ;;
315
+ 2)
316
+ if [ -n "$noninteractive" ]; then
317
+ printf 'sudo: a password is required (polkit requires authentication for %s)\\n' "$ACTION" >&2
318
+ else
319
+ printf 'pi-polkit: sudo: no polkit authentication agent answered\\n' >&2
320
+ fi
321
+ exit 1
322
+ ;;
323
+ 3)
324
+ printf 'pi-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
325
+ exit 126
326
+ ;;
327
+ *)
328
+ printf 'pi-polkit: sudo: polkit check failed (status %s)\\n' "$status" >&2
329
+ exit 127
330
+ ;;
331
+ esac
332
+ fi
333
+
334
+ # -k drops what polkit itself recorded. The password a desktop agent keeps is
335
+ # the agent's, not polkit's, so say that it stays.
336
+ if [ "$mode" = revoke ]; then
337
+ pkcheck --revoke-temp || exit 127
338
+ if [ $# -eq 0 ]; then
339
+ printf 'pi-polkit: %s\\n' 'revoked the temporary polkit authorizations; a desktop agent that caches the password keeps its own copy, so the next command may still not ask' >&2
340
+ exit 0
103
341
  fi
104
- exit $status
342
+ fi
343
+
344
+ # -E: pkexec strips the environment, so name the variables again inside the shell
345
+ # that runs the command. Entries reach the helper as arguments, so spaces, quotes
346
+ # and newlines survive exactly; the helper also drops every shim directory from
347
+ # PATH, so an elevated command cannot resolve sudo back to a shim.
348
+ shim_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null)
349
+ env_helper=$shim_dir/pi-polkit-env
350
+ [ -x "$env_helper" ] || fail "the pi-polkit environment helper is missing: $env_helper"
351
+
352
+ # Runs in the caller's shell, never in a command substitution: an exit there
353
+ # would only end the subshell, and a name reaches eval below, so it must be a
354
+ # name before that happens.
355
+ check_preserve() {
356
+ if [ -z "$preserve_names" ]; then
357
+ command -v xargs >/dev/null 2>&1 || fail '-E needs xargs to forward the environment; name the variables instead: --preserve-env=NAME[,NAME...]'
358
+ return 0
359
+ fi
360
+ old_ifs=$IFS
361
+ IFS=,
362
+ for name in $preserve_names; do
363
+ case $name in
364
+ '' | [0-9]* | *[!A-Za-z0-9_]*)
365
+ IFS=$old_ifs
366
+ fail "not a variable name: $name"
367
+ ;;
368
+ esac
369
+ done
370
+ IFS=$old_ifs
105
371
  }
106
372
 
373
+ export_prefix() {
374
+ if [ -n "$preserve_names" ]; then
375
+ set --
376
+ old_ifs=$IFS
377
+ IFS=,
378
+ for name in $preserve_names; do
379
+ is_set=
380
+ eval "is_set=\\\${$name+yes}"
381
+ [ -n "$is_set" ] || continue
382
+ eval "value=\\\${$name}"
383
+ set -- "$@" "$name=$value"
384
+ done
385
+ IFS=$old_ifs
386
+ "$env_helper" "$@"
387
+ return
388
+ fi
389
+ env -0 | xargs -0 -n 64 "$env_helper"
390
+ }
391
+
392
+ require_without_prompt
393
+
394
+ [ $# -gt 0 ] || fail 'sudo needs a command: sudo [OPTIONS] COMMAND (an interactive shell needs a terminal)'
395
+
396
+ # -s runs the command through a shell, which pkexec expresses exactly.
397
+ if [ "$mode" = shell ]; then
398
+ set -- /bin/sh -c "$*"
399
+ fi
400
+
401
+ # The preserved environment is exported by a shell prologue, so the command keeps
402
+ # its own argv rather than being flattened into one string.
403
+ if [ -n "$preserve" ]; then
404
+ check_preserve
405
+ prefix=$(export_prefix)
406
+ set -- /bin/sh -c "\${prefix}exec \\"\\$@\\"" sh "$@"
407
+ fi
408
+
107
409
  if [ -n "$user" ]; then
108
- run_pkexec --user "$user" "$@"
410
+ set -- --user "$user" "$@"
109
411
  fi
412
+
413
+ # -b returns as soon as the command is started, as sudo -b does.
414
+ if [ -n "$background" ]; then
415
+ pkexec "$@" &
416
+ exit 0
417
+ fi
418
+
110
419
  run_pkexec "$@"
111
420
  `
112
421
 
@@ -115,14 +424,13 @@ const DOAS_SHIM = `#!/bin/sh
115
424
  # pi-polkit: doas shim. Routes doas to pkexec so polkit can authorize it.
116
425
  # Generated at extension startup; do not edit.
117
426
 
118
- fail() {
119
- printf 'pi-polkit: %s\\n' "$1" >&2
120
- exit ${SHIM_USAGE_EXIT}
121
- }
122
-
123
- command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
124
-
427
+ ${SHIM_PRELUDE}
428
+ ${SHIM_CHECKS}
429
+ PROG=doas
125
430
  user=
431
+ noninteractive=
432
+ mode=
433
+
126
434
  while [ $# -gt 0 ]; do
127
435
  case $1 in
128
436
  --)
@@ -136,14 +444,15 @@ while [ $# -gt 0 ]; do
136
444
  ;;
137
445
  -u?*)
138
446
  user=\${1#-u}
139
- [ -n "$user" ] || fail 'doas -u needs a username: -u USER PROGRAM'
140
447
  shift
141
448
  ;;
142
449
  -n)
143
- fail 'doas -n is not supported: pkexec has no non-interactive mode. Drop -n.'
450
+ noninteractive=1
451
+ shift
144
452
  ;;
145
453
  -s)
146
- fail 'doas -s opens a shell; run pkexec bash instead.'
454
+ mode=shell
455
+ shift
147
456
  ;;
148
457
  -C | -a | -L)
149
458
  fail "option has no pkexec equivalent: $1"
@@ -157,20 +466,18 @@ while [ $# -gt 0 ]; do
157
466
  esac
158
467
  done
159
468
 
160
- # pkexec is silent when the dialog is dismissed (status 126), so say what the
161
- # status means. Every other failure path already prints its own reason.
162
- run_pkexec() {
163
- pkexec "$@"
164
- status=$?
165
- if [ $status -eq 126 ]; then
166
- printf 'pi-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
167
- fi
168
- exit $status
169
- }
469
+ require_without_prompt
470
+
471
+ [ $# -gt 0 ] || fail 'doas needs a command: doas [OPTIONS] COMMAND (an interactive shell needs a terminal)'
472
+
473
+ if [ "$mode" = shell ]; then
474
+ set -- /bin/sh -c "$*"
475
+ fi
170
476
 
171
477
  if [ -n "$user" ]; then
172
- run_pkexec --user "$user" "$@"
478
+ set -- --user "$user" "$@"
173
479
  fi
480
+
174
481
  run_pkexec "$@"
175
482
  `
176
483
 
@@ -184,13 +491,9 @@ const SU_SHIM = `#!/bin/sh
184
491
  # pi-polkit: su shim. Routes su -c to pkexec so polkit can authorize it.
185
492
  # Generated at extension startup; do not edit.
186
493
 
187
- fail() {
188
- printf 'pi-polkit: %s\\n' "$1" >&2
189
- exit ${SHIM_USAGE_EXIT}
190
- }
191
-
192
- command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
193
-
494
+ ${SHIM_PRELUDE}
495
+ ${SHIM_CHECKS}
496
+ PROG=su
194
497
  user=
195
498
  cmd=
196
499
  has_cmd=
@@ -271,26 +574,72 @@ done
271
574
  [ -n "$has_cmd" ] || fail 'su without -c opens an interactive shell; run pkexec bash instead.'
272
575
 
273
576
  # su keeps the working directory unless it is a login shell, and --keep-cwd is
274
- # how pkexec does the same. pkexec is silent when the dialog is dismissed
275
- # (status 126), so say what the status means.
276
- pkexec --keep-cwd --user "$user" sh -c "$cmd"
277
- status=$?
278
- if [ $status -eq 126 ]; then
279
- printf 'pi-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
280
- fi
281
- exit $status
577
+ # how pkexec does the same.
578
+ run_pkexec --keep-cwd --user "$user" sh -c "$cmd"
579
+ `
580
+
581
+ /**
582
+ * The environment helper: turns `NAME=VALUE` arguments into an export statement
583
+ * each, so `-E` can rebuild the environment pkexec stripped. Values arrive as
584
+ * arguments rather than text, so spaces, quotes and newlines need no escaping on
585
+ * the way in. PATH keeps every entry except the shim directories.
586
+ */
587
+ const POLKIT_ENV_HELPER = `#!/bin/sh
588
+ # pi-polkit: rebuilds a stripped environment. Generated at extension startup;
589
+ # do not edit.
590
+
591
+ for entry in "$@"; do
592
+ name=\${entry%%=*}
593
+ value=\${entry#*=}
594
+ case $name in
595
+ '' | [0-9]* | *[!A-Za-z0-9_]*)
596
+ continue
597
+ ;;
598
+ PATH)
599
+ filtered=
600
+ old_ifs=$IFS
601
+ IFS=:
602
+ for dir in $value; do
603
+ case \${dir##*/} in
604
+ pi-polkit-*)
605
+ continue
606
+ ;;
607
+ esac
608
+ filtered=$filtered\${filtered:+:}$dir
609
+ done
610
+ IFS=$old_ifs
611
+ value=$filtered
612
+ ;;
613
+ esac
614
+ quote="'"
615
+ quoted=
616
+ while :; do
617
+ case $value in
618
+ *"$quote"*)
619
+ quoted=$quoted\${value%%"$quote"*}"'\\\\''"
620
+ value=\${value#*"$quote"}
621
+ ;;
622
+ *)
623
+ quoted=$quoted$value
624
+ break
625
+ ;;
626
+ esac
627
+ done
628
+ printf "export '%s=%s'; " "$name" "$quoted"
629
+ done
282
630
  `
283
631
 
632
+
284
633
  /** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
285
634
  const SUDOEDIT_SHIM = `#!/bin/sh
286
- printf 'pi-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
287
- exit ${SHIM_USAGE_EXIT}
635
+ ${SHIM_PRELUDE}
636
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
288
637
  `
289
638
 
290
639
  /** visudo needs an interactive editor on a terminal the shell tool does not have. */
291
640
  const VISUDO_SHIM = `#!/bin/sh
292
- printf 'pi-polkit: visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.\\n' >&2
293
- exit ${SHIM_USAGE_EXIT}
641
+ ${SHIM_PRELUDE}
642
+ fail 'visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.'
294
643
  `
295
644
 
296
645
  /** File name → script body. Names must match the commands being redirected. */
@@ -300,6 +649,7 @@ const POLKIT_SHIM_SCRIPTS: Record<string, string> = {
300
649
  su: SU_SHIM,
301
650
  sudoedit: SUDOEDIT_SHIM,
302
651
  visudo: VISUDO_SHIM,
652
+ "pi-polkit-env": POLKIT_ENV_HELPER,
303
653
  }
304
654
 
305
655
  /** Shim directory of the running extension; created on first use, cleared by removePolkitShims. */