pi-polkit 0.2.0 → 0.2.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,39 @@ 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.1] - 2026-11-07
9
+
10
+ ### Added
11
+
12
+ - `test/polkit-shims.test.ts`: behaviour tests for the generated shims. They
13
+ stub `pkexec`, `pkcheck` and `pkaction` on `PATH`, so they need neither polkit,
14
+ a 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 bash tool's prompt guideline 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 Pi.
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
+
8
41
  ## [0.2.0] - 2026-11-05
9
42
 
10
43
  ### Added
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # pi-polkit
2
2
 
3
3
  Redirects `sudo`, `doas` and `su -c` to `pkexec` inside Pi, so authorisation
4
- goes through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal
5
- the shell tools do not have. Applies to the `bash` tool and to `!` user
6
- commands; `sudo` in other terminals is unchanged.
4
+ goes through the desktop's polkit agent instead of a terminal the shell tools do
5
+ not have. Applies to the `bash` tool and to `!` user commands; `sudo` in other
6
+ terminals is unchanged.
7
7
 
8
8
  ## Scope
9
9
 
@@ -47,7 +47,7 @@ pi -e ./pi-polkit
47
47
  | `sudo -u bob …` | as above, as `bob` |
48
48
  | `cat x \| sudo tee y` | dialog for `tee` |
49
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 |
50
+ | `sudo -v` | authenticates through polkit; creates no timestamp |
51
51
  | `sudo -l` | prints the polkit policy and this session's temporary authorizations |
52
52
  | `sudo -k`, `-K` | revokes polkit's temporary authorizations |
53
53
  | `sudo -H`, `-N`, `-P` | accepted: pkexec already does what they ask |
@@ -93,20 +93,66 @@ pkexec stripped.
93
93
  one; other actions may not, according to policy. The extension takes no part in
94
94
  that decision.
95
95
 
96
+ `sudoers` is not consulted: polkit policy decides who may run what, so per-user
97
+ and per-command rules, `NOPASSWD`, `secure_path` and `env_keep` do not apply.
98
+ Authentication runs through the `polkit-1` PAM service, not `/etc/pam.d/sudo`,
99
+ so a module configured only for sudo — a fingerprint reader, a second factor —
100
+ is not used here; the service's own stack (normally `system-auth`) decides how
101
+ the password is verified.
102
+
96
103
  `org.freedesktop.policykit.exec` asks for administrator authentication on every
97
- call, but the desktop agent may keep the password it just received and answer
98
- the next challenge itself (KDE's agent does). A second command in the same
99
- session then runs without a dialog. That cache belongs to the agent: another
100
- agent, a restarted agent or a dismissed dialog means the next command prompts
101
- again, so do not rely on it.
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.
104
+ call, but an agent may keep the password it just received and answer a later
105
+ challenge itself. In that case a second command can run without a dialog. That
106
+ cache belongs to the agent: a different agent, a restarted agent or a change in
107
+ the agent's state can mean the next command prompts again, so do not rely on it.
108
+
109
+ With the default policy, polkit records no temporary authorization for this
110
+ action: after a successful `pkexec`, `pkcheck --list-temp` stays empty and a
111
+ non-interactive `pkcheck` still reports that authentication is required. Whether
112
+ a later command prompts is therefore the agent's decision, and no user-space
113
+ query can predict it. That is why `sudo -n` reports what the policy says rather
114
+ than guessing whether a dialog would appear, and why `sudo -k` can only clear
115
+ polkit's own record.
116
+
117
+ The extension keeps no authorization state of its own: `sudo -v` verifies once
118
+ and writes nothing, `sudo -n` assumes nothing, and `sudo -k` only clears polkit's
119
+ record. Authorising one command therefore leaves the next one to be decided
120
+ again, which is the point — the person who allowed an upgrade can still refuse
121
+ the deletion that follows. What widens that window is the agent's password
122
+ cache, not this extension, and nothing here can revoke it.
123
+
124
+ ## Account lockout
125
+
126
+ When the `polkit-1` PAM service includes `pam_faillock` (Arch's default stack
127
+ does), the dialog shares one failure tally per user with terminal sudo, `su` and
128
+ the login screen:
129
+
130
+ - A wrong or empty password submitted in the dialog adds a record. Dismissing
131
+ the dialog, closing it or pressing Esc cancels the agent's helper before PAM
132
+ returns, and adds none. (An agent process that is killed instead of
133
+ cancelling leaves the helper at EOF, and that attempt can count.)
134
+ - An agent may allow several attempts in one dialog, and each retry starts a
135
+ new PAM helper; a dialog can therefore add several records and use up the
136
+ default `deny=3`.
137
+ - Once a user has `deny` failures within `fail_interval` (900 s by default),
138
+ `pam_faillock` refuses that user in every service using the stack for
139
+ `unlock_time` (600 s by default, measured from the newest record). A locked
140
+ account rejects even the correct password, because `preauth` fails and
141
+ `authsucc` cannot reset the tally; further attempts neither extend nor shorten
142
+ the lock. A successful password authentication outside a lock resets the
143
+ tally.
144
+ - `sudo -k` clears polkit's authorizations, not the tally. The locked user's own
145
+ authentication cannot succeed, so Pi cannot elevate with that user's
146
+ credentials to reset it: wait for the lock to expire, use a root console or
147
+ another administrator, or reboot (`/run` is cleared). Where the tally file is
148
+ readable by the user, `faillock --user "$USER"` shows the records and
149
+ `faillock --user "$USER" --reset` clears them without root; otherwise both
150
+ need root. Check readability with `ls -l /run/faillock/$USER`.
151
+
152
+ Retrying a failed dialog in a loop only gives the user more chances to submit a
153
+ wrong or empty password, and the lock applies to the whole account. The prompt
154
+ guideline therefore tells the model to ask before retrying, and `sudo -n` remains
155
+ the probe that never invokes PAM.
110
156
 
111
157
  ## Limitations
112
158
 
@@ -116,6 +162,10 @@ dialog would appear, and why `sudo -k` can only clear polkit's own record.
116
162
  - `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
117
163
  account that real `su` would allow can be refused here.
118
164
  - `su -c` runs the command with `sh -c`, not with the target user's login shell.
165
+ - `-n`, `-v` and `-l` check `org.freedesktop.policykit.exec` itself. pkexec may
166
+ select a different action installed with an
167
+ `org.freedesktop.policykit.exec.path` (and `argv1`) annotation, so that
168
+ action's rules are not reflected by these checks.
119
169
  - `sudo -E` re-exports the environment through `pkexec`'s argument list, so the
120
170
  values are visible in `ps` while the command starts. Exported shell functions
121
171
  are skipped, and `PATH` loses the shim directories.
@@ -135,18 +185,19 @@ without one, `pkexec` fails instead of prompting.
135
185
 
136
186
  | Status | Meaning |
137
187
  |--------|--------------------------------------------------------|
138
- | 1 | `-n` or `-v` could not be satisfied: polkit requires authentication, or refuses |
188
+ | 1 | `-n` or `-v` could not be satisfied: polkit requires authentication, denies it, or authentication failed |
139
189
  | 126 | the polkit dialog was dismissed |
140
- | 127 | not authorized, or the program could not run |
190
+ | 127 | not authorized (including after failed authentication), or the program could not run |
141
191
  | 64 | the extension refused the invocation |
142
192
 
143
193
  Messages are in English.
144
194
 
145
195
  ## Development
146
196
 
147
- `npm install && npm run check` type-checks against the pinned
148
- `@earendil-works/pi-coding-agent` 1.4.0, the API Pi serves to Node
149
- extensions.
197
+ `npm install`, then `npm run check` type-checks against the pinned
198
+ `@earendil-works/pi-coding-agent` 1.4.0, the API Pi serves to Node extensions,
199
+ and `npm test` runs the shim behaviour tests. The tests stub `pkexec`, `pkcheck`
200
+ and `pkaction` on `PATH`, so they need neither polkit, a desktop agent nor root.
150
201
 
151
202
  ## License
152
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-polkit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": {
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "check": "tsc -p tsconfig.json --noEmit",
17
- "prepublishOnly": "npm run check"
17
+ "test": "node --experimental-strip-types --test test/*.test.ts",
18
+ "prepublishOnly": "npm run check && npm test"
18
19
  },
19
20
  "peerDependencies": {
20
21
  "@earendil-works/pi-coding-agent": "*"
@@ -4,9 +4,9 @@ import { polkitShimsPathExportLine, removePolkitShims, withPolkitShimsOnPath } f
4
4
 
5
5
  /**
6
6
  * Redirects sudo, doas and su -c to pkexec inside Pi, so authorisation goes
7
- * through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal the
8
- * shell tools do not have. Applies to the `bash` tool and to `!` user
9
- * commands; `sudo` in other terminals is unchanged.
7
+ * through the desktop's polkit agent instead of a terminal the shell tools do
8
+ * not have. Applies to the `bash` tool and to `!` user commands; `sudo` in other
9
+ * terminals is unchanged.
10
10
  *
11
11
  * The redirect is a PATH prepend, not a command rewrite: the shims in
12
12
  * polkit-shims.ts stand in for the sudo/doas/su binaries, and the shell's own
@@ -25,7 +25,7 @@ export default function polkitRedirect(pi: ExtensionAPI): void {
25
25
  // The model plans privilege with what it knows about sudo. Say once, in the
26
26
  // system prompt, how this shell differs, so it does not learn it by failing.
27
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.",
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 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.",
29
29
  )
30
30
  pi.registerTool(bash)
31
31
 
package/polkit-shims.ts CHANGED
@@ -309,7 +309,7 @@ if [ "$mode" = validate ]; then
309
309
  exit 0
310
310
  ;;
311
311
  1)
312
- printf 'pi-polkit: sudo: polkit denies %s to this process\\n' "$ACTION" >&2
312
+ printf 'pi-polkit: sudo: polkit did not authorize %s (the policy denies it, or authentication failed)\\n' "$ACTION" >&2
313
313
  exit 1
314
314
  ;;
315
315
  2)