opencode-polkit 0.3.1 → 0.4.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,90 @@ 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.4.1] - 2026-11-07
9
+
10
+ ### Added
11
+
12
+ - `test/shims.test.ts`: behaviour tests for the generated shims. They stub
13
+ `pkexec`, `pkcheck` and `pkaction` on `PATH`, so they need neither polkit, a
14
+ desktop agent nor root; `npm test` runs them with Node's test runner.
15
+ - README: account-lockout section. Where the `polkit-1` PAM service includes
16
+ `pam_faillock`, a wrong or empty password submitted in the dialog adds a
17
+ record shared with terminal sudo and login; dismissing the dialog adds none,
18
+ an agent may allow several attempts in one dialog (each retry starts a new PAM
19
+ helper), and a locked account rejects even the correct password for
20
+ `unlock_time`. Documents how to inspect and reset the tally, and why retrying
21
+ does not help.
22
+ - README limitation: `-n`, `-v` and `-l` check
23
+ `org.freedesktop.policykit.exec` itself, while pkexec may select another
24
+ action installed with `org.freedesktop.policykit.exec.path`/`argv1`
25
+ annotations.
26
+
27
+ ### Changed
28
+
29
+ - The shell tool's description now asks the model to consult the user before
30
+ retrying a dismissed or failed dialog, because failed or empty password
31
+ submissions count toward a system-wide `pam_faillock` lockout while a dismissal
32
+ does not.
33
+ - README: `sudoers` and the `sudo` PAM stack are not consulted; polkit policy and
34
+ the `polkit-1` PAM service decide, so sudo-only modules such as a fingerprint
35
+ reader or a second factor do not run in OpenCode.
36
+ - README now describes agent caching and retry behaviour generically instead of
37
+ attributing it to one desktop agent.
38
+ - `sudo -v` reports an interactive failure as "polkit did not authorize" instead
39
+ of "polkit denies", since the check can fail after the password is rejected.
40
+ - The shim's action comment now records that pkexec may select a custom action by
41
+ annotation, which the checks do not see.
42
+ - `npm run check` now also type-checks `test/`, and CI runs check, test and
43
+ build.
44
+
45
+ ## [0.4.0] - 2026-11-05
46
+
47
+ ### Added
48
+
49
+ - `sudo -n`, `doas -n`: a non-interactive `pkcheck` on
50
+ `org.freedesktop.policykit.exec` decides whether the command runs. polkit grants
51
+ it without asking, or the shim exits `1` with `sudo: a password is required` and
52
+ the reason that dropping `-n` lets the agent answer. Measured: a successful
53
+ `pkexec` leaves no temporary authorization behind (`pkcheck --list-temp` stays
54
+ empty), so the silent second run is the agent's own password cache and no
55
+ non-interactive query can predict it; `-n` therefore reports policy, not a
56
+ guess.
57
+ - `sudo -v` performs the authentication (`pkcheck --allow-user-interaction`) and
58
+ exits `0`, `1`, `126` when the dialog is dismissed, or `127`; `sudo -nv` uses
59
+ the non-interactive check.
60
+ - `sudo -l` prints `pkaction --verbose` for the action and this session's
61
+ temporary authorizations.
62
+ - `sudo -k`/`-K` revoke polkit's temporary authorizations and say that an agent's
63
+ own password cache cannot be cleared from here.
64
+ - `sudo -H`, `-N`, `-P` are accepted as no-ops: pkexec already sets `HOME`, keeps
65
+ no timestamp and leaves groups alone.
66
+ - `sudo -s COMMAND` and `--shell` run the command through a shell, `sudo -b`
67
+ starts it in the background and returns, and `sudo -E`/
68
+ `--preserve-env[=LIST]` re-export the environment inside the command.
69
+ - `sudo --help` and `sudo -V` are answered by the real binary, so scripts that
70
+ probe sudo keep working.
71
+ - Short options bundle (`sudo -nu bob`), and unknown long options report their own
72
+ name instead of `--`.
73
+ - `opencode-polkit-env`: helper that turns `NAME=VALUE` arguments into export
74
+ statements. `-E` passes entries through argv (`env -0` into `xargs -0`), so
75
+ spaces, quotes and newlines survive, and it drops every shim directory from
76
+ `PATH` so an elevated command cannot resolve sudo back to a shim.
77
+ - `src/index.ts`: the shell tool's description gains one sentence naming the
78
+ redirect and what `-n`, `-v` and `-l` answer, so the model plans privilege with
79
+ the real contract instead of learning it from a failed call.
80
+
81
+ ### Changed
82
+
83
+ - The shims share one prologue: `fail`, the polkit subject (pinned with
84
+ `pid,start-time,uid` as `pkcheck(1)` requires), the two checks and the `pkexec`
85
+ wrapper.
86
+ - `-n` failures exit `1` like sudo, not `64`: `64` now means only that the
87
+ invocation cannot be expressed. README documents the four-way handling
88
+ (translated, no-op, checked, refused) and polkit's missing temp authorization.
89
+ - `sudo -i`, `-S`, `-A`, `sudoedit` and `visudo` still refuse, now from the shared
90
+ prelude.
91
+
8
92
  ## [0.3.1] - 2026-11-04
9
93
 
10
94
  ### Added
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # opencode-polkit
2
2
 
3
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.
4
+ authorisation goes through the desktop's polkit agent 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.
7
7
 
8
8
  ## Scope
9
9
 
@@ -48,8 +48,16 @@ For a local checkout, run `npm run build` once, then reference the directory:
48
48
  | `sudo apt install x` | polkit dialog, then runs as root |
49
49
  | `sudo -u bob …` | as above, as `bob` |
50
50
  | `cat x \| sudo tee y` | dialog for `tee` |
51
- | `sudo -n …` | refused: pkexec has no non-interactive mode |
52
- | `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
51
+ | `sudo -n …` | runs only if polkit would authorise without asking; otherwise exit 1 with `sudo: a password is required` |
52
+ | `sudo -v` | authenticates through polkit; creates no timestamp |
53
+ | `sudo -l` | prints the polkit policy and this session's temporary authorizations |
54
+ | `sudo -k`, `-K` | revokes polkit's temporary authorizations |
55
+ | `sudo -H`, `-N`, `-P` | accepted: pkexec already does what they ask |
56
+ | `sudo -s CMD` | runs `CMD` through a shell, as root |
57
+ | `sudo -E …` | runs with the caller's environment |
58
+ | `sudo -b …` | starts the command in the background |
59
+ | `sudo --help`, `-V` | answered by the real sudo; nothing elevates |
60
+ | `sudo -i`, `-S`, `-A` | refused, with the equivalent command |
53
61
  | `sudoedit`, `visudo` | refused, with the alternative |
54
62
  | `su -c 'id'` | dialog, then runs as root |
55
63
  | `su -c 'id' bob` | as above, as `bob` (administrator password) |
@@ -57,7 +65,9 @@ For a local checkout, run `npm run build` once, then reference the directory:
57
65
  | `su -m`, `-s`, `-g`, `-w`, `--pty` | refused, with the alternative |
58
66
  | `pkexec …` | passed through |
59
67
 
60
- Refused invocations exit with status 64 and one line naming the alternative.
68
+ An option is refused only when polkit cannot express it: it needs a terminal, a
69
+ password channel polkit does not have, or an editor. Refusals exit with status 64
70
+ and one line naming the alternative.
61
71
 
62
72
  ## How it works
63
73
 
@@ -66,18 +76,82 @@ resolves those instead of the system binaries, and they forward the command to
66
76
  `pkexec`. Command text is never rewritten, so quoting, pipes and subshells need
67
77
  no special handling.
68
78
 
79
+ The `create.before` shell hook prepends the stand-in directory to the spawned
80
+ `PATH`. The directory lives in `$XDG_RUNTIME_DIR` (falling back to `/tmp`) as
81
+ `0700`, is removed when the plugin is unloaded, and also holds
82
+ `opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
83
+ pkexec stripped.
84
+
85
+ The shell tool's description gains a note about the redirect, so the model
86
+ plans privilege with the real contract instead of learning it from a failed
87
+ call.
88
+
69
89
  ## Authorisation
70
90
 
71
91
  `polkitd` decides whether a password is required. These commands always require
72
92
  one; other actions may not, according to policy. The plugin takes no part in
73
93
  that decision.
74
94
 
95
+ `sudoers` is not consulted: polkit policy decides who may run what, so per-user
96
+ and per-command rules, `NOPASSWD`, `secure_path` and `env_keep` do not apply.
97
+ Authentication runs through the `polkit-1` PAM service, not `/etc/pam.d/sudo`,
98
+ so a module configured only for sudo — a fingerprint reader, a second factor —
99
+ is not used here; the service's own stack (normally `system-auth`) decides how
100
+ the password is verified.
101
+
75
102
  `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.
103
+ call, but an agent may keep the password it just received and answer a later
104
+ challenge itself. In that case a second command can run without a dialog. That
105
+ cache belongs to the agent: a different agent, a restarted agent or a change in
106
+ the agent's state can mean the next command prompts again, so do not rely on it.
107
+
108
+ With the default policy, polkit records no temporary authorization for this
109
+ action: after a successful `pkexec`, `pkcheck --list-temp` stays empty and a
110
+ non-interactive `pkcheck` still reports that authentication is required. Whether
111
+ a later command prompts is therefore the agent's decision, and no user-space
112
+ query can predict it. That is why `sudo -n` reports what the policy says rather
113
+ than guessing whether a dialog would appear, and why `sudo -k` can only clear
114
+ polkit's own record.
115
+
116
+ The plugin keeps no authorization state of its own: `sudo -v` verifies once and
117
+ writes nothing, `sudo -n` assumes nothing, and `sudo -k` only clears polkit's
118
+ record. Authorising one command therefore leaves the next one to be decided
119
+ again, which is the point — the person who allowed an upgrade can still refuse
120
+ the deletion that follows. What widens that window is the agent's password
121
+ cache, not this plugin, and nothing here can revoke it.
122
+
123
+ ## Account lockout
124
+
125
+ When the `polkit-1` PAM service includes `pam_faillock` (Arch's default stack
126
+ does), the dialog shares one failure tally per user with terminal sudo, `su` and
127
+ the login screen:
128
+
129
+ - A wrong or empty password submitted in the dialog adds a record. Dismissing
130
+ the dialog, closing it or pressing Esc cancels the agent's helper before PAM
131
+ returns, and adds none. (An agent process that is killed instead of
132
+ cancelling leaves the helper at EOF, and that attempt can count.)
133
+ - An agent may allow several attempts in one dialog, and each retry starts a
134
+ new PAM helper; a dialog can therefore add several records and use up the
135
+ default `deny=3`.
136
+ - Once a user has `deny` failures within `fail_interval` (900 s by default),
137
+ `pam_faillock` refuses that user in every service using the stack for
138
+ `unlock_time` (600 s by default, measured from the newest record). A locked
139
+ account rejects even the correct password, because `preauth` fails and
140
+ `authsucc` cannot reset the tally; further attempts neither extend nor shorten
141
+ the lock. A successful password authentication outside a lock resets the
142
+ tally.
143
+ - `sudo -k` clears polkit's authorizations, not the tally. The locked user's own
144
+ authentication cannot succeed, so OpenCode cannot elevate with that user's
145
+ credentials to reset it: wait for the lock to expire, use a root console or
146
+ another administrator, or reboot (`/run` is cleared). Where the tally file is
147
+ readable by the user, `faillock --user "$USER"` shows the records and
148
+ `faillock --user "$USER" --reset` clears them without root; otherwise both
149
+ need root. Check readability with `ls -l /run/faillock/$USER`.
150
+
151
+ Retrying a failed dialog in a loop only gives the user more chances to submit a
152
+ wrong or empty password, and the lock applies to the whole account. The shell
153
+ tool's description therefore tells the model to ask before retrying, and
154
+ `sudo -n` remains the probe that never invokes PAM.
81
155
 
82
156
  ## Limitations
83
157
 
@@ -87,6 +161,15 @@ again, so do not rely on it.
87
161
  - `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
88
162
  account that real `su` would allow can be refused here.
89
163
  - `su -c` runs the command with `sh -c`, not with the target user's login shell.
164
+ - `-n`, `-v` and `-l` check `org.freedesktop.policykit.exec` itself. pkexec may
165
+ select a different action installed with an
166
+ `org.freedesktop.policykit.exec.path` (and `argv1`) annotation, so that
167
+ action's rules are not reflected by these checks.
168
+ - `sudo -E` re-exports the environment through `pkexec`'s argument list, so the
169
+ values are visible in `ps` while the command starts. Exported shell functions
170
+ are skipped, and `PATH` loses the shim directories.
171
+ - `sudo -k` cannot clear the password a desktop agent holds; only polkit's
172
+ temporary authorizations are revoked.
90
173
 
91
174
  These bypass the redirect, not polkit, which still decides whether the command
92
175
  runs.
@@ -98,14 +181,23 @@ without one, `pkexec` fails instead of prompting.
98
181
 
99
182
  ## Exit status
100
183
 
101
- | Status | Meaning |
102
- |--------|----------------------------------------------|
103
- | 126 | the polkit dialog was dismissed |
104
- | 127 | not authorised, or the program could not run |
105
- | 64 | the plugin refused the invocation |
184
+ | Status | Meaning |
185
+ |--------|--------------------------------------------------------|
186
+ | 1 | `-n` or `-v` could not be satisfied: polkit requires authentication, denies it, or authentication failed |
187
+ | 126 | the polkit dialog was dismissed |
188
+ | 127 | not authorised (including after failed authentication), or the program could not run |
189
+ | 64 | the plugin refused the invocation |
106
190
 
107
191
  Messages are in English.
108
192
 
193
+ ## Development
194
+
195
+ `npm install`, then `npm run check` type-checks the plugin and its tests, and
196
+ `npm test` runs the shim behaviour tests. The tests stub `pkexec`, `pkcheck` and
197
+ `pkaction` on `PATH`, so they need neither polkit, a desktop agent nor root;
198
+ they need Node 22.19 or later for `--experimental-strip-types`. `npm run build`
199
+ regenerates `dist`.
200
+
109
201
  ## License
110
202
 
111
203
  MIT
package/dist/index.js CHANGED
@@ -4,6 +4,16 @@ import { installPolkitShims, removePolkitShims } from "./shims.js";
4
4
  * Only the prepend matters; the rest is a last-resort fallback.
5
5
  */
6
6
  const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
7
+ /** Tool names whose description should name how elevation works here. */
8
+ const SHELL_TOOLS = ["bash", "shell"];
9
+ /** Opening words of the appended description; also the guard against appending twice. */
10
+ const CONTRACT_MARKER = "polkit redirect:";
11
+ /**
12
+ * Appended to the shell tool's description. The model plans privilege with what
13
+ * it knows about sudo, so the difference is stated where it reads the tool,
14
+ * instead of being learned from a failed call.
15
+ */
16
+ const SHELL_TOOL_CONTRACT = `${CONTRACT_MARKER} \`sudo\`, \`doas\` and \`su -c\` run through 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 a real authentication, and \`sudo -l\` prints the polkit policy that decides. Ask the user before retrying a dismissed or failed dialog instead of looping: on systems with pam_faillock, failed or empty password submissions count toward a system-wide lockout shared with login, while dismissing the dialog does not.`;
7
17
  /**
8
18
  * Redirects sudo/doas/su -c to pkexec without touching the command text.
9
19
  *
@@ -26,6 +36,20 @@ const setup = async (ctx) => {
26
36
  if (!path.startsWith(prefix))
27
37
  event.env.PATH = prefix + path;
28
38
  });
39
+ // The shell tool is the host's, so its description is extended rather than
40
+ // replaced. Every reload runs this transform again, so a description that
41
+ // already carries the line is left alone.
42
+ await ctx.tool.transform((tools) => {
43
+ for (const tool of tools.list()) {
44
+ if (!SHELL_TOOLS.includes(tool.id) && !SHELL_TOOLS.includes(tool.name))
45
+ continue;
46
+ tools.update(tool.id, (entry) => {
47
+ if (entry.description.includes(CONTRACT_MARKER))
48
+ return;
49
+ entry.description = `${entry.description}\n\n${SHELL_TOOL_CONTRACT}`;
50
+ });
51
+ }
52
+ });
29
53
  return () => removePolkitShims(shimDir);
30
54
  };
31
55
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE;;;GAGG;AACH,MAAM,aAAa,GAAG,8DAA8D,CAAA;AAEpF;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,KAAK,EAAE,GAAmB,EAA2B,EAAE;IACnE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAA;IACpC,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAA;IAE5B,8EAA8E;IAC9E,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAA;QAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;AACzC,CAAC,CAAA;AAED;;;GAGG;AACH,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE;;;GAGG;AACH,MAAM,aAAa,GAAG,8DAA8D,CAAA;AAEpF,yEAAyE;AACzE,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErC,yFAAyF;AACzF,MAAM,eAAe,GAAG,kBAAkB,CAAA;AAE1C;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,GAAG,eAAe,wiBAAwiB,CAAA;AAEtlB;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,KAAK,EAAE,GAAmB,EAA2B,EAAE;IACnE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAA;IACpC,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAA;IAE5B,8EAA8E;IAC9E,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAA;QAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,2EAA2E;IAC3E,0EAA0E;IAC1E,0CAA0C;IAC1C,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAQ;YAChF,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC9B,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,eAAe,CAAC;oBAAE,OAAM;gBACvD,KAAK,CAAC,WAAW,GAAG,GAAG,KAAK,CAAC,WAAW,OAAO,mBAAmB,EAAE,CAAA;YACtE,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;AACzC,CAAC,CAAA;AAED;;;GAGG;AACH,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAA0B,CAAA"}
package/dist/shims.d.ts CHANGED
@@ -9,7 +9,9 @@ export declare const SHIM_USAGE_EXIT = 64;
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
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.
12
+ * is 0700 so no other local user can rewrite the scripts, and it holds
13
+ * `opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
14
+ * pkexec stripped.
13
15
  */
14
16
  export declare function installPolkitShims(): string;
15
17
  /** Deletes a directory created by installPolkitShims. */
package/dist/shims.js CHANGED
@@ -8,82 +8,261 @@ import { isAbsolute, join } from "node:path";
8
8
  * any output.
9
9
  */
10
10
  export const SHIM_USAGE_EXIT = 64;
11
+ /** The action pkexec checks; the checks below ask polkit about the same one. */
12
+ const POLKIT_ACTION = "org.freedesktop.policykit.exec";
11
13
  /**
12
- * The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not
13
- * this plugin — decides that `sudo` means this file. It forwards to pkexec
14
- * and never parses what pkexec prints: the exit status is the whole result.
15
- *
16
- * Only options with an exact pkexec equivalent are translated. Everything
17
- * else is refused with an actionable message rather than forwarded, because
18
- * pkexec would reject it with a message that does not say what to do instead.
14
+ * The part every shim starts with: one refusal helper, and a check that the two
15
+ * polkit tools exist at all.
19
16
  */
20
- const SUDO_SHIM = `#!/bin/sh
21
- # opencode-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
22
- # Generated at plugin startup; do not edit.
23
-
24
- fail() {
17
+ const SHIM_PRELUDE = `fail() {
25
18
  printf 'opencode-polkit: %s\\n' "$1" >&2
26
19
  exit ${SHIM_USAGE_EXIT}
27
20
  }
28
21
 
29
22
  command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
23
+ command -v pkcheck >/dev/null 2>&1 || fail 'pkcheck is not installed; install polkit'
24
+ `;
25
+ /**
26
+ * The checks that answer what sudo's timestamp file used to answer, plus the
27
+ * pkexec wrapper. Only exit statuses are read: pkcheck's messages and its
28
+ * `polkit.result` detail are localised and addressed to humans, so parsing them
29
+ * would break on the next locale or polkit release.
30
+ */
31
+ const SHIM_CHECKS = `# The action checked below. pkexec uses it by default; a site can install
32
+ # another action selected by org.freedesktop.policykit.exec.path/argv1, whose
33
+ # rules these checks do not see.
34
+ ACTION=${POLKIT_ACTION}
30
35
 
36
+ # pkcheck(1) warns that a bare pid races with pid reuse, so pin the start time.
37
+ subject() {
38
+ started=$(awk '{ print $22 }' /proc/$$/stat 2>/dev/null)
39
+ if [ -n "$started" ]; then
40
+ printf '%s,%s,%s' "$$" "$started" "$(id -u)"
41
+ else
42
+ printf '%s' "$$"
43
+ fi
44
+ }
45
+
46
+ # 0 grants the action without asking anyone, 2 requires authentication,
47
+ # 1 refuses it. pkcheck's own 126 (usage) and 127 (error) pass through.
48
+ check_without_prompt() {
49
+ pkcheck --action-id "$ACTION" --process "$(subject)" >/dev/null 2>&1
50
+ }
51
+
52
+ # The same check with the desktop agent allowed to answer. An agent that still
53
+ # holds the password answers without a dialog, which is why a 0 here also means
54
+ # the next pkexec runs as root without asking.
55
+ check_with_prompt() {
56
+ pkcheck --action-id "$ACTION" --process "$(subject)" --allow-user-interaction >/dev/null 2>&1
57
+ }
58
+
59
+ # -n means "run only if no one has to be asked". polkit cannot say whether the
60
+ # agent would answer from its cache, so a 2 is reported as needing a password
61
+ # and the command is not run: that is what -n promises, in both directions.
62
+ require_without_prompt() {
63
+ if [ -n "$noninteractive" ]; then
64
+ check_without_prompt
65
+ status=$?
66
+ case $status in
67
+ 0)
68
+ return 0
69
+ ;;
70
+ 1)
71
+ printf 'opencode-polkit: %s: polkit denies %s to this process\\n' "$PROG" "$ACTION" >&2
72
+ exit 1
73
+ ;;
74
+ 2)
75
+ 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
76
+ exit 1
77
+ ;;
78
+ *)
79
+ printf 'opencode-polkit: %s: polkit check failed (status %s)\\n' "$PROG" "$status" >&2
80
+ exit 127
81
+ ;;
82
+ esac
83
+ fi
84
+ return 0
85
+ }
86
+
87
+ # pkexec is silent when the dialog is dismissed (status 126), so say what the
88
+ # status means. Every other failure path already prints its own reason.
89
+ run_pkexec() {
90
+ pkexec "$@"
91
+ status=$?
92
+ if [ $status -eq 126 ]; then
93
+ printf 'opencode-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
94
+ fi
95
+ exit $status
96
+ }
97
+ `;
98
+ /**
99
+ * The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not this
100
+ * plugin — decides that `sudo` means this file. It forwards to pkexec and
101
+ * never parses what pkexec prints: the exit status is the whole result.
102
+ *
103
+ * Options are handled in four ways, and only the last one refuses. Those whose
104
+ * effect pkexec already has (`-H`, `-N`, `-P`) are accepted as no-ops; those
105
+ * polkit answers better than a run (`-n`, `-v`, `-l`, `-k`) become checks; those
106
+ * a wrapper can express (`-s COMMAND`, `-E`, `-b`) are translated; the rest,
107
+ * which need a terminal or a password channel polkit does not have, are refused
108
+ * with one line naming the alternative.
109
+ */
110
+ const SUDO_SHIM = `#!/bin/sh
111
+ # opencode-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
112
+ # Generated at plugin startup; do not edit.
113
+
114
+ ${SHIM_PRELUDE}
115
+ ${SHIM_CHECKS}
116
+ PROG=sudo
31
117
  user=
118
+ noninteractive=
119
+ preserve=
120
+ preserve_names=
121
+ background=
122
+ mode=
123
+
32
124
  while [ $# -gt 0 ]; do
33
125
  case $1 in
34
126
  --)
35
127
  shift
36
128
  break
37
129
  ;;
38
- -u)
39
- [ $# -ge 2 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
40
- user=$2
41
- shift 2
42
- ;;
43
130
  --user)
44
131
  [ $# -ge 2 ] || fail 'sudo --user needs a username: --user USER PROGRAM'
45
132
  user=$2
46
133
  shift 2
47
134
  ;;
48
- -u?*)
49
- user=\${1#-u}
50
- [ -n "$user" ] || fail 'sudo -u needs a username: -u USER PROGRAM'
51
- shift
52
- ;;
53
135
  --user=*)
54
136
  user=\${1#--user=}
55
- [ -n "$user" ] || fail 'sudo --user needs a username: --user USER PROGRAM'
56
137
  shift
57
138
  ;;
58
- -n | --non-interactive)
59
- fail 'sudo -n is not supported: pkexec has no non-interactive mode, polkit decides whether authentication is needed. Drop -n.'
139
+ --non-interactive)
140
+ noninteractive=1
141
+ shift
142
+ ;;
143
+ --preserve-env)
144
+ preserve=1
145
+ shift
146
+ ;;
147
+ --preserve-env=*)
148
+ preserve=1
149
+ preserve_names=\${1#--preserve-env=}
150
+ shift
151
+ ;;
152
+ --set-home | --no-update | --preserve-groups)
153
+ shift
60
154
  ;;
61
- -v | --validate | -k | --reset-timestamp | -K | --remove-timestamp)
62
- fail 'sudo credential caching does not exist under polkit; drop -v/-k.'
155
+ --list)
156
+ mode=list
157
+ shift
63
158
  ;;
64
- -l | --list)
65
- fail 'sudo -l is not available under polkit; inspect policy with pkaction --verbose.'
159
+ --validate)
160
+ mode=validate
161
+ shift
66
162
  ;;
67
- -e | --edit)
68
- fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
163
+ --reset-timestamp | --remove-timestamp)
164
+ mode=revoke
165
+ shift
69
166
  ;;
70
- -i | --login | -s | --shell)
71
- fail 'sudo -i/-s opens a login shell; run pkexec bash instead.'
167
+ --background)
168
+ background=1
169
+ shift
72
170
  ;;
73
- -S | --stdin)
74
- fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
171
+ --help)
172
+ mode=help
173
+ shift
75
174
  ;;
76
- -A | --askpass)
77
- fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
175
+ --version)
176
+ mode=version
177
+ shift
78
178
  ;;
79
- -b | --background)
80
- fail 'sudo -b backgrounds the command; append & instead.'
179
+ --stdin | --askpass | --edit | --login)
180
+ case $1 in
181
+ --edit)
182
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
183
+ ;;
184
+ --stdin)
185
+ fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
186
+ ;;
187
+ --askpass)
188
+ fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
189
+ ;;
190
+ *)
191
+ fail 'sudo -i opens a login shell; run pkexec bash instead.'
192
+ ;;
193
+ esac
194
+ ;;
195
+ --shell)
196
+ mode=shell
197
+ shift
81
198
  ;;
82
- -E | --preserve-env | --preserve-env=* | -H | --set-home | -P | --preserve-groups | -N | --no-update)
83
- fail 'pkexec deliberately sanitizes the environment; drop -E/-H/-P.'
199
+ --*)
200
+ fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
84
201
  ;;
85
202
  -?*)
86
- fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
203
+ rest=\${1#-}
204
+ shift
205
+ while [ -n "$rest" ]; do
206
+ letter=\${rest%"\${rest#?}"}
207
+ rest=\${rest#?}
208
+ case $letter in
209
+ u)
210
+ if [ -n "$rest" ]; then
211
+ user=$rest
212
+ rest=
213
+ else
214
+ [ $# -ge 1 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
215
+ user=$1
216
+ shift
217
+ fi
218
+ ;;
219
+ n)
220
+ noninteractive=1
221
+ ;;
222
+ E)
223
+ preserve=1
224
+ ;;
225
+ H | N | P)
226
+ # pkexec already sets HOME, keeps no timestamp and leaves groups alone.
227
+ ;;
228
+ b)
229
+ background=1
230
+ ;;
231
+ l)
232
+ mode=list
233
+ ;;
234
+ v)
235
+ mode=validate
236
+ ;;
237
+ k | K)
238
+ mode=revoke
239
+ ;;
240
+ s)
241
+ mode=shell
242
+ ;;
243
+ h)
244
+ mode=help
245
+ ;;
246
+ V)
247
+ mode=version
248
+ ;;
249
+ i)
250
+ fail 'sudo -i opens a login shell; run pkexec bash instead.'
251
+ ;;
252
+ e)
253
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
254
+ ;;
255
+ S)
256
+ fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
257
+ ;;
258
+ A)
259
+ fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
260
+ ;;
261
+ *)
262
+ fail "option has no pkexec equivalent: -$letter (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
263
+ ;;
264
+ esac
265
+ done
87
266
  ;;
88
267
  *)
89
268
  break
@@ -91,20 +270,148 @@ while [ $# -gt 0 ]; do
91
270
  esac
92
271
  done
93
272
 
94
- # pkexec is silent when the dialog is dismissed (status 126), so say what the
95
- # status means. Every other failure path already prints its own reason.
96
- run_pkexec() {
97
- pkexec "$@"
273
+ # --help and --version describe the binary, not the authorisation, so nothing is
274
+ # elevated: answering them exactly keeps scripts that probe sudo working.
275
+ if [ "$mode" = help ] || [ "$mode" = version ]; then
276
+ for candidate in /usr/bin/sudo /bin/sudo /usr/local/bin/sudo; do
277
+ [ -x "$candidate" ] || continue
278
+ if [ "$mode" = help ]; then
279
+ exec "$candidate" --help
280
+ fi
281
+ exec "$candidate" --version
282
+ done
283
+ fail 'the real sudo is not installed, so --help and --version cannot be answered'
284
+ fi
285
+
286
+ # -l reports the policy polkit will apply and the authorizations this session
287
+ # already holds. Both halves are local and never prompt.
288
+ if [ "$mode" = list ]; then
289
+ pkaction --action-id "$ACTION" --verbose || exit 127
290
+ printf '\\nopencode-polkit: temporary authorizations in this session\\n'
291
+ pkcheck --list-temp || exit 127
292
+ exit 0
293
+ fi
294
+
295
+ # -v asks the same question sudo -v asks: can authentication succeed right now?
296
+ if [ "$mode" = validate ]; then
297
+ if [ -n "$noninteractive" ]; then
298
+ check_without_prompt
299
+ else
300
+ check_with_prompt
301
+ fi
98
302
  status=$?
99
- if [ $status -eq 126 ]; then
100
- printf 'opencode-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
303
+ case $status in
304
+ 0)
305
+ exit 0
306
+ ;;
307
+ 1)
308
+ printf 'opencode-polkit: sudo: polkit did not authorize %s (the policy denies it, or authentication failed)\\n' "$ACTION" >&2
309
+ exit 1
310
+ ;;
311
+ 2)
312
+ if [ -n "$noninteractive" ]; then
313
+ printf 'sudo: a password is required (polkit requires authentication for %s)\\n' "$ACTION" >&2
314
+ else
315
+ printf 'opencode-polkit: sudo: no polkit authentication agent answered\\n' >&2
316
+ fi
317
+ exit 1
318
+ ;;
319
+ 3)
320
+ printf 'opencode-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
321
+ exit 126
322
+ ;;
323
+ *)
324
+ printf 'opencode-polkit: sudo: polkit check failed (status %s)\\n' "$status" >&2
325
+ exit 127
326
+ ;;
327
+ esac
328
+ fi
329
+
330
+ # -k drops what polkit itself recorded. The password a desktop agent keeps is
331
+ # the agent's, not polkit's, so say that it stays.
332
+ if [ "$mode" = revoke ]; then
333
+ pkcheck --revoke-temp || exit 127
334
+ if [ $# -eq 0 ]; then
335
+ printf 'opencode-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
336
+ exit 0
101
337
  fi
102
- exit $status
338
+ fi
339
+
340
+ # -E: pkexec strips the environment, so name the variables again inside the shell
341
+ # that runs the command. Entries reach the helper as arguments, so spaces, quotes
342
+ # and newlines survive exactly; the helper also drops every shim directory from
343
+ # PATH, so an elevated command cannot resolve sudo back to a shim.
344
+ shim_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null)
345
+ env_helper=$shim_dir/opencode-polkit-env
346
+ [ -x "$env_helper" ] || fail "the opencode-polkit environment helper is missing: $env_helper"
347
+
348
+ # Runs in the caller's shell, never in a command substitution: an exit there
349
+ # would only end the subshell, and a name reaches eval below, so it must be a
350
+ # name before that happens.
351
+ check_preserve() {
352
+ if [ -z "$preserve_names" ]; then
353
+ command -v xargs >/dev/null 2>&1 || fail '-E needs xargs to forward the environment; name the variables instead: --preserve-env=NAME[,NAME...]'
354
+ return 0
355
+ fi
356
+ old_ifs=$IFS
357
+ IFS=,
358
+ for name in $preserve_names; do
359
+ case $name in
360
+ '' | [0-9]* | *[!A-Za-z0-9_]*)
361
+ IFS=$old_ifs
362
+ fail "not a variable name: $name"
363
+ ;;
364
+ esac
365
+ done
366
+ IFS=$old_ifs
103
367
  }
104
368
 
369
+ export_prefix() {
370
+ if [ -n "$preserve_names" ]; then
371
+ set --
372
+ old_ifs=$IFS
373
+ IFS=,
374
+ for name in $preserve_names; do
375
+ is_set=
376
+ eval "is_set=\\\${$name+yes}"
377
+ [ -n "$is_set" ] || continue
378
+ eval "value=\\\${$name}"
379
+ set -- "$@" "$name=$value"
380
+ done
381
+ IFS=$old_ifs
382
+ "$env_helper" "$@"
383
+ return
384
+ fi
385
+ env -0 | xargs -0 -n 64 "$env_helper"
386
+ }
387
+
388
+ require_without_prompt
389
+
390
+ [ $# -gt 0 ] || fail 'sudo needs a command: sudo [OPTIONS] COMMAND (an interactive shell needs a terminal)'
391
+
392
+ # -s runs the command through a shell, which pkexec expresses exactly.
393
+ if [ "$mode" = shell ]; then
394
+ set -- /bin/sh -c "$*"
395
+ fi
396
+
397
+ # The preserved environment is exported by a shell prologue, so the command keeps
398
+ # its own argv rather than being flattened into one string.
399
+ if [ -n "$preserve" ]; then
400
+ check_preserve
401
+ prefix=$(export_prefix)
402
+ set -- /bin/sh -c "\${prefix}exec \\"\\$@\\"" sh "$@"
403
+ fi
404
+
105
405
  if [ -n "$user" ]; then
106
- run_pkexec --user "$user" "$@"
406
+ set -- --user "$user" "$@"
407
+ fi
408
+
409
+ # -b returns as soon as the command is started, as sudo -b does.
410
+ if [ -n "$background" ]; then
411
+ pkexec "$@" &
412
+ exit 0
107
413
  fi
414
+
108
415
  run_pkexec "$@"
109
416
  `;
110
417
  /** The doas shim: same contract, doas option grammar. */
@@ -112,14 +419,13 @@ const DOAS_SHIM = `#!/bin/sh
112
419
  # opencode-polkit: doas shim. Routes doas to pkexec so polkit can authorize it.
113
420
  # Generated at plugin startup; do not edit.
114
421
 
115
- fail() {
116
- printf 'opencode-polkit: %s\\n' "$1" >&2
117
- exit ${SHIM_USAGE_EXIT}
118
- }
119
-
120
- command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
121
-
422
+ ${SHIM_PRELUDE}
423
+ ${SHIM_CHECKS}
424
+ PROG=doas
122
425
  user=
426
+ noninteractive=
427
+ mode=
428
+
123
429
  while [ $# -gt 0 ]; do
124
430
  case $1 in
125
431
  --)
@@ -133,14 +439,15 @@ while [ $# -gt 0 ]; do
133
439
  ;;
134
440
  -u?*)
135
441
  user=\${1#-u}
136
- [ -n "$user" ] || fail 'doas -u needs a username: -u USER PROGRAM'
137
442
  shift
138
443
  ;;
139
444
  -n)
140
- fail 'doas -n is not supported: pkexec has no non-interactive mode. Drop -n.'
445
+ noninteractive=1
446
+ shift
141
447
  ;;
142
448
  -s)
143
- fail 'doas -s opens a shell; run pkexec bash instead.'
449
+ mode=shell
450
+ shift
144
451
  ;;
145
452
  -C | -a | -L)
146
453
  fail "option has no pkexec equivalent: $1"
@@ -154,20 +461,18 @@ while [ $# -gt 0 ]; do
154
461
  esac
155
462
  done
156
463
 
157
- # pkexec is silent when the dialog is dismissed (status 126), so say what the
158
- # status means. Every other failure path already prints its own reason.
159
- run_pkexec() {
160
- pkexec "$@"
161
- status=$?
162
- if [ $status -eq 126 ]; then
163
- printf 'opencode-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
164
- fi
165
- exit $status
166
- }
464
+ require_without_prompt
465
+
466
+ [ $# -gt 0 ] || fail 'doas needs a command: doas [OPTIONS] COMMAND (an interactive shell needs a terminal)'
467
+
468
+ if [ "$mode" = shell ]; then
469
+ set -- /bin/sh -c "$*"
470
+ fi
167
471
 
168
472
  if [ -n "$user" ]; then
169
- run_pkexec --user "$user" "$@"
473
+ set -- --user "$user" "$@"
170
474
  fi
475
+
171
476
  run_pkexec "$@"
172
477
  `;
173
478
  /**
@@ -180,13 +485,9 @@ const SU_SHIM = `#!/bin/sh
180
485
  # opencode-polkit: su shim. Routes su -c to pkexec so polkit can authorize it.
181
486
  # Generated at plugin startup; do not edit.
182
487
 
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
-
488
+ ${SHIM_PRELUDE}
489
+ ${SHIM_CHECKS}
490
+ PROG=su
190
491
  user=
191
492
  cmd=
192
493
  has_cmd=
@@ -234,7 +535,7 @@ while [ $# -gt 0 ]; do
234
535
  fail 'su -w whitelists environment variables; pkexec sanitizes the environment.'
235
536
  ;;
236
537
  -P | --pty)
237
- fail 'su --pty asks for a terminal; the shell tool has none.'
538
+ fail 'su --pty asks for a terminal; the bash tool has none.'
238
539
  ;;
239
540
  -f | --fast)
240
541
  fail 'su -f is a shell flag; pass it to the shell in the command instead.'
@@ -267,24 +568,68 @@ done
267
568
  [ -n "$has_cmd" ] || fail 'su without -c opens an interactive shell; run pkexec bash instead.'
268
569
 
269
570
  # 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
571
+ # how pkexec does the same.
572
+ run_pkexec --keep-cwd --user "$user" sh -c "$cmd"
573
+ `;
574
+ /**
575
+ * The environment helper: turns `NAME=VALUE` arguments into an export statement
576
+ * each, so `-E` can rebuild the environment pkexec stripped. Values arrive as
577
+ * arguments rather than text, so spaces, quotes and newlines need no escaping on
578
+ * the way in. PATH keeps every entry except the shim directories.
579
+ */
580
+ const POLKIT_ENV_HELPER = `#!/bin/sh
581
+ # opencode-polkit: rebuilds a stripped environment. Generated at plugin startup;
582
+ # do not edit.
583
+
584
+ for entry in "$@"; do
585
+ name=\${entry%%=*}
586
+ value=\${entry#*=}
587
+ case $name in
588
+ '' | [0-9]* | *[!A-Za-z0-9_]*)
589
+ continue
590
+ ;;
591
+ PATH)
592
+ filtered=
593
+ old_ifs=$IFS
594
+ IFS=:
595
+ for dir in $value; do
596
+ case \${dir##*/} in
597
+ opencode-polkit-*)
598
+ continue
599
+ ;;
600
+ esac
601
+ filtered=$filtered\${filtered:+:}$dir
602
+ done
603
+ IFS=$old_ifs
604
+ value=$filtered
605
+ ;;
606
+ esac
607
+ quote="'"
608
+ quoted=
609
+ while :; do
610
+ case $value in
611
+ *"$quote"*)
612
+ quoted=$quoted\${value%%"$quote"*}"'\\\\''"
613
+ value=\${value#*"$quote"}
614
+ ;;
615
+ *)
616
+ quoted=$quoted$value
617
+ break
618
+ ;;
619
+ esac
620
+ done
621
+ printf "export '%s=%s'; " "$name" "$quoted"
622
+ done
278
623
  `;
279
624
  /** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
280
625
  const SUDOEDIT_SHIM = `#!/bin/sh
281
- printf 'opencode-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
282
- exit ${SHIM_USAGE_EXIT}
626
+ ${SHIM_PRELUDE}
627
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
283
628
  `;
284
629
  /** visudo needs an interactive editor on a terminal the shell tool does not have. */
285
630
  const VISUDO_SHIM = `#!/bin/sh
286
- printf 'opencode-polkit: visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.\\n' >&2
287
- exit ${SHIM_USAGE_EXIT}
631
+ ${SHIM_PRELUDE}
632
+ fail 'visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.'
288
633
  `;
289
634
  /** File name → script body. Names must match the commands being redirected. */
290
635
  const POLKIT_SHIM_SCRIPTS = {
@@ -293,12 +638,15 @@ const POLKIT_SHIM_SCRIPTS = {
293
638
  su: SU_SHIM,
294
639
  sudoedit: SUDOEDIT_SHIM,
295
640
  visudo: VISUDO_SHIM,
641
+ "opencode-polkit-env": POLKIT_ENV_HELPER,
296
642
  };
297
643
  /**
298
644
  * Writes the shim scripts into a fresh private directory and returns it.
299
645
  * The caller prepends this directory to PATH; the shell then resolves
300
646
  * `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.
647
+ * is 0700 so no other local user can rewrite the scripts, and it holds
648
+ * `opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
649
+ * pkexec stripped.
302
650
  */
303
651
  export function installPolkitShims() {
304
652
  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;;;;;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"}
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,gFAAgF;AAChF,MAAM,aAAa,GAAG,gCAAgC,CAAA;AAEtD;;;GAGG;AACH,MAAM,YAAY,GAAG;;QAEb,eAAe;;;;;CAKtB,CAAA;AAED;;;;;GAKG;AACH,MAAM,WAAW,GAAG;;;SAGX,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+DrB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,SAAS,GAAG;;;;EAIhB,YAAY;EACZ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6SZ,CAAA;AAED,yDAAyD;AACzD,MAAM,SAAS,GAAG;;;;EAIhB,YAAY;EACZ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDZ,CAAA;AAED;;;;;GAKG;AACH,MAAM,OAAO,GAAG;;;;EAId,YAAY;EACZ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFZ,CAAA;AAED;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CzB,CAAA;AAGD,8EAA8E;AAC9E,MAAM,aAAa,GAAG;EACpB,YAAY;;CAEb,CAAA;AAED,qFAAqF;AACrF,MAAM,WAAW,GAAG;EAClB,YAAY;;CAEb,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;IACnB,qBAAqB,EAAE,iBAAiB;CACzC,CAAA;AAED;;;;;;;GAOG;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,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-polkit",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
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",
@@ -20,7 +20,9 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
23
- "check": "tsc -p tsconfig.json --noEmit"
23
+ "check": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
24
+ "test": "node --experimental-strip-types --test test/*.test.ts",
25
+ "prepublishOnly": "npm run check && npm test && npm run build"
24
26
  },
25
27
  "engines": {
26
28
  "opencode": ">=2.0.29"