opencode-polkit 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-11-01
9
+
10
+ 0.3.0 replaces how the plugin decides anything. It used to parse the command
11
+ string to find `sudo`, then parse the output to guess whether authentication
12
+ failed; both are gone. The plugin now only prepends a directory of shims to
13
+ `PATH`, letting the shell's own name resolution pick them, and reads the
14
+ outcome from exit status alone. Everything that existed to support guessing —
15
+ the lexical scanner, the output matchers, the denial cache, the translations —
16
+ is deleted with it. The plugin no longer changes the command the agent wrote.
17
+
18
+ ### Added
19
+
20
+ - `src/shims.ts`: `sudo`, `doas`, `sudoedit` and `visudo` scripts written to a
21
+ private `0700` temporary directory at setup and removed when the plugin is
22
+ disposed. `sudo` and `doas` forward to `pkexec`; `sudoedit` and `visudo`
23
+ refuse, because polkit has no equivalent flow.
24
+ - Deterministic option contract in the shims: `-u USER`, `-uUSER`, `--user USER`
25
+ and `--user=USER` become `pkexec --user USER`, `--` ends option parsing, and
26
+ every other option is refused with exit status `64` and a message naming the
27
+ alternative (`-n` → polkit decides, `-i`/`-s` → `pkexec bash`, `-E`/`-H`/`-P`
28
+ → pkexec sanitizes the environment, `-b` → append `&`). `pkexec` would
29
+ otherwise reject them with `Cannot run program`, which says nothing about what
30
+ to do next.
31
+ - A one-line explanation on exit status `126`: it is the only pkexec failure
32
+ path that prints nothing of its own, so the shim says the dialog was
33
+ dismissed. Every `127` path already prints its reason.
34
+ - `SHIM_USAGE_EXIT` (64) documents that the refusal status is the shim's own and
35
+ cannot be confused with pkexec's `126`/`127`.
36
+
37
+ ### Changed
38
+
39
+ - **Breaking:** commands are no longer rewritten. The `shell` tool's
40
+ `execute.before`/`execute.after` hooks are replaced by a single
41
+ `shell.create.before` hook that only sets `event.env.PATH`, so the agent's
42
+ command reaches the shell exactly as written, and `sudo` inside quotes,
43
+ comments or heredocs needs no special case.
44
+ - **Breaking:** authentication failures are reported from exit status
45
+ (`126` dismissed, `127` not authorized, `64` refused invocation), never from
46
+ output. The locale-dependent matchers for `Not authorized`,
47
+ `Error executing command as another user` and
48
+ `Error creating textual authentication agent` are gone.
49
+ - **Breaking:** messages are English only. The `Messages` table, the ten
50
+ translations and `detectLocale` are removed; the consumer is the coding agent.
51
+ - **Breaking:** a refused command is no longer remembered. Retrying after a
52
+ dismissed dialog prompts again instead of being rejected by the plugin.
53
+ - PATH interception is idempotent: prepending happens once, so stacked hooks do
54
+ not grow `PATH`.
55
+
56
+ ### Removed
57
+
58
+ - `scanPrivileged` (the quote-, escape- and heredoc-aware lexical scanner) and
59
+ `findBadOption`.
60
+ - The `deniedCommands` and `originalCommands` maps, along with the
61
+ `execute.after` result rewriting they supported.
62
+ - The `translations` table for en, zh, ja, ko, de, fr, es, pt, ru, tr and uk.
63
+ - The i18n section of the README, which now documents the exit-status contract
64
+ and the PATH shim's limits instead.
package/README.md CHANGED
@@ -1,87 +1,82 @@
1
1
  # opencode-polkit
2
2
 
3
- Redirect `sudo`/`doas` to `pkexec` in [OpenCode](https://opencode.ai).
4
- Triggers the system's native polkit authentication dialog (KDE, GNOME, etc.)
5
- instead of requiring a terminal for password input.
3
+ Redirects `sudo` and `doas` to `pkexec` inside OpenCode, so authorisation goes
4
+ through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal the
5
+ shell tool does not have. Applies to commands run by the shell tool; `sudo` in
6
+ other terminals is unchanged.
6
7
 
7
- Like CachyOS Hello, the plugin does not check for a polkit agent up front:
8
- `sudo` is always redirected to `pkexec` at execution time, and polkitd routes
9
- the request to whatever agent is currently registered. Without an agent,
10
- `pkexec` fails fast with `Error creating textual authentication agent`
11
- (no TTY); the plugin reports that as a clear denial. A hang (dialog never
12
- appears) is bounded by the bash tool's own timeout (default 2 min,
13
- configurable up to 10 min) and left untranslated — it cannot be told
14
- apart from a long-running command.
15
-
16
- ## Install
8
+ ## Installation
17
9
 
18
10
  ```sh
19
- opencode plugin opencode-polkit
11
+ opencode plugin add opencode-polkit
20
12
  ```
21
13
 
22
- Or add to `opencode.json` / `~/.config/opencode/opencode.jsonc`:
14
+ Or add it to `opencode.json` / `~/.config/opencode/opencode.jsonc`:
23
15
 
24
16
  ```json
25
17
  {
26
- "plugin": ["opencode-polkit"]
18
+ "plugins": ["opencode-polkit"]
27
19
  }
28
20
  ```
29
21
 
30
- For local development, point opencode at the project directory instead:
22
+ For a local checkout, run `npm run build` once, then reference the directory:
31
23
 
32
24
  ```json
33
25
  {
34
- "plugin": ["/path/to/opencode-polkit"]
26
+ "plugins": ["/path/to/opencode-polkit"]
35
27
  }
36
28
  ```
37
29
 
38
30
  ## Behavior
39
31
 
40
- | Command | result |
41
- |-----------------------|-------------------------------------------------------|
42
- | `sudo xxx` | redirects to `pkexec xxx` |
43
- | `doas xxx` | redirects to `pkexec xxx` |
44
- | `sudo -n xxx` | blocked: pkexec has no short options (unclear failure)|
45
- | `cat x \| sudo tee y` | redirects to `cat x \| pkexec tee y` (mid-command) |
46
- | `pkexec xxx` | passes through |
47
- | `sudoedit` / `visudo` | blocked |
48
-
49
- ### Minimal intervention
50
-
51
- The command is rewritten **only** by replacing `sudo`/`doas` with
52
- `pkexec` — its shape is otherwise untouched, so the agent always sees a
53
- command that behaves like the one it wrote. Privilege keywords are found
54
- by a lexical scanner (quote/escape/heredoc aware): `sudo` inside string
55
- literals, comments or heredocs is ignored; `sudo` in any executable
56
- position (leading, after `&&`/`||`/`|`, in `$(...)` or subshells, after
57
- an env assignment like `FOO=1 sudo x`) is rewritten, while the same word
58
- in argument position (`--name sudo bash`) is left alone. `sudo a && sudo b`
59
- rewrites both.
60
-
61
- ### Failures
62
-
63
- Unambiguous authentication failures are reported as clear errors and the
64
- command is remembered so a retry is rejected without prompting again
65
- (cleared when opencode restarts): `Not authorized`, `Error executing
66
- command as another user`, `Error creating textual authentication agent`
67
- (no polkit agent + no TTY). A hang (dialog never appears) is bounded by
68
- the bash tool's own timeout (default 2 min, configurable up to 10 min)
69
- and is left untranslated — it cannot be told apart from a long-running
70
- command.
71
-
72
- Options pkexec does not accept (all short options, plus any long option
73
- outside `--user`, `--keep-cwd`, `--disable-internal-agent`, `--help`,
74
- `--version`) are rejected before execution with a clear message, since
75
- the rewrite would otherwise surface pkexec's own confusing
76
- `Cannot run program -n` error. `sudo --` and options after the program
77
- name are left alone.
78
-
79
- ## i18n
80
-
81
- Messages adapt to `$LC_MESSAGES` / `$LANG`. Currently supports:
82
- en, zh, ja, ko, de, fr, es, pt, ru, tr, uk.
83
-
84
- PRs welcome for additional translations.
32
+ | Command | Result |
33
+ |------------------------|---------------------------------------------|
34
+ | `sudo apt install x` | polkit dialog, then runs as root |
35
+ | `sudo -u bob …` | as above, as `bob` |
36
+ | `cat x \| sudo tee y` | dialog for `tee` |
37
+ | `sudo -n …` | refused: pkexec has no non-interactive mode |
38
+ | `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
39
+ | `sudoedit`, `visudo` | refused, with the alternative |
40
+ | `pkexec …` | passed through |
41
+
42
+ Refused invocations exit with status 64 and one line naming the alternative.
43
+
44
+ ## How it works
45
+
46
+ `sudo` and `doas` stand-ins are placed first on `PATH`, so the shell resolves
47
+ those instead of the system binaries, and they forward the command to `pkexec`.
48
+ Command text is never rewritten, so quoting, pipes and subshells need no special
49
+ handling.
50
+
51
+ ## Authorisation
52
+
53
+ `polkitd` decides whether a password is required. These commands always require
54
+ one; other actions may not, according to policy. The plugin takes no part in
55
+ that decision.
56
+
57
+ ## Limitations
58
+
59
+ - Absolute paths (`/usr/bin/sudo`) and relative ones (`./sudo`) bypass `PATH`.
60
+ - Commands that reset `PATH` (`env -i`, `PATH=/usr/bin …`) bypass it as well.
61
+ - Programs that invoke `sudo` with a scrubbed environment bypass it.
62
+
63
+ These bypass the redirect, not polkit, which still decides whether the command
64
+ runs.
65
+
66
+ ## Requirements
67
+
68
+ `polkit` and a registered authentication agent. Desktop environments start one;
69
+ without one, `pkexec` fails instead of prompting.
70
+
71
+ ## Exit status
72
+
73
+ | Status | Meaning |
74
+ |--------|----------------------------------------------|
75
+ | 126 | the polkit dialog was dismissed |
76
+ | 127 | not authorised, or the program could not run |
77
+ | 64 | the plugin refused the invocation |
78
+
79
+ Messages are in English.
85
80
 
86
81
  ## License
87
82
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,10 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const PolkitPlugin: Plugin;
3
- //# sourceMappingURL=index.d.ts.map
1
+ import type { Plugin } from "@opencode/plugin";
2
+ /**
3
+ * OpenCode 2.0 requires a default export shaped `{ id, setup }`. Type-only
4
+ * imports keep the built plugin free of runtime dependencies.
5
+ */
6
+ declare const _default: {
7
+ id: string;
8
+ setup: (ctx: Plugin.Context) => Promise<Plugin.Cleanup>;
9
+ };
10
+ export default _default;
package/dist/index.js CHANGED
@@ -1,305 +1,36 @@
1
- /** Long options pkexec accepts (all of them; pkexec has no short options). */
2
- const PKEXEC_OPTIONS = new Set([
3
- "--version",
4
- "--help",
5
- "--disable-internal-agent",
6
- "--keep-cwd",
7
- "--user",
8
- ]);
1
+ import { installPolkitShims, removePolkitShims } from "./shims.js";
9
2
  /**
10
- * Lexical scan for privilege-escalation keywords OUTSIDE quotes.
11
- *
12
- * Shell commands may contain `sudo` anywhere a command starts (pipes,
13
- * `&&`/`||`, subshells, command substitution), and string literals /
14
- * comments may contain the same words spuriously. A plain regex cannot
15
- * tell those apart; this scanner tracks quote state (' " and backslash
16
- * escapes) and only reports keywords in executable position.
17
- *
18
- * - `node -e "…sudo…"` → no hit (inside quotes)
19
- * - `cat x | sudo tee y` → hit at index 8 (command boundary)
20
- * - `cd /tmp && sudo ./x` → hit (after `&&`)
21
- * - `--name sudo bash` → no hit (argument position)
22
- * - `echo x; # sudo y` → no hit (comment)
3
+ * PATH used when the shell tool supplies none, mirroring a normal Linux host.
4
+ * Only the prepend matters; the rest is a last-resort fallback.
23
5
  */
24
- function scanPrivileged(command) {
25
- const hits = [];
26
- let inSingle = false;
27
- let inDouble = false;
28
- let escaped = false;
29
- // True right after a command boundary (`;`, `&&`, `||`, `|`, `(`, start).
30
- let atCommandStart = true;
31
- // Heredoc delimiters waiting to close (stack; multiple << are legal).
32
- const heredocs = [];
33
- for (let i = 0; i < command.length; i++) {
34
- const ch = command[i];
35
- // Inside a heredoc body: only a line consisting of the delimiter ends
36
- // it. Content there is a string literal, never a command. Pop the
37
- // matching delimiter (nested heredocs close in any order).
38
- if (heredocs.length > 0) {
39
- const lineEnd = command.indexOf("\n", i);
40
- const line = lineEnd === -1 ? command.slice(i) : command.slice(i, lineEnd);
41
- const idx = heredocs.lastIndexOf(line);
42
- if (idx !== -1) {
43
- heredocs.splice(idx);
44
- // The line after the delimiter starts a new command position.
45
- atCommandStart = true;
46
- }
47
- i = lineEnd === -1 ? command.length : lineEnd;
48
- continue;
49
- }
50
- if (escaped) {
51
- escaped = false;
52
- continue;
53
- }
54
- if (ch === "\\" && !inSingle) {
55
- escaped = true;
56
- continue;
57
- }
58
- if (ch === "'" && !inDouble) {
59
- inSingle = !inSingle;
60
- continue;
61
- }
62
- if (ch === '"' && !inDouble) {
63
- inDouble = !inDouble;
64
- continue;
65
- }
66
- if (inSingle || inDouble)
67
- continue;
68
- // Heredoc start: `<<[-]DELIM` (delim may be quoted or unquoted).
69
- if (ch === "<" && command[i + 1] === "<") {
70
- let j = i + 2;
71
- if (command[j] === "-")
72
- j++;
73
- while (j < command.length && /\s/.test(command[j]))
74
- j++;
75
- if (j < command.length && (command[j] === "'" || command[j] === '"')) {
76
- const q = command[j];
77
- const end = command.indexOf(q, j + 1);
78
- if (end !== -1) {
79
- heredocs.push(command.slice(j + 1, end));
80
- i = end;
81
- continue;
82
- }
83
- }
84
- else {
85
- const m = /^[^\s|;&<>()]+/.exec(command.slice(j));
86
- if (m) {
87
- heredocs.push(m[0]);
88
- i = j + m[0].length - 1;
89
- continue;
90
- }
91
- }
92
- }
93
- // Command boundaries open a new executable position.
94
- if (ch === ";" || ch === "|" || ch === "&" || ch === "(" || ch === "\n") {
95
- atCommandStart = true;
96
- continue;
97
- }
98
- // `#` at a word start begins a comment that runs to end of line.
99
- if (ch === "#") {
100
- const prev = command[i - 1] ?? "";
101
- if (i === 0 || /\s/.test(prev) || /[;|&(]/.test(prev)) {
102
- const nl = command.indexOf("\n", i);
103
- i = nl === -1 ? command.length : nl;
104
- continue;
105
- }
106
- }
107
- if (/\s/.test(ch))
108
- continue;
109
- // An env assignment (`FOO=1 sudo x`) still starts a command; any other
110
- // word in argument position (`--name sudo`) does not.
111
- let isCommandPos = atCommandStart;
112
- if (!isCommandPos) {
113
- let j = i - 1;
114
- while (j >= 0 && /\s/.test(command[j]))
115
- j--;
116
- while (j >= 0 && !/[\s;|&()]/.test(command[j]))
117
- j--;
118
- const token = command.slice(j + 1, i);
119
- if (token.includes("="))
120
- isCommandPos = true;
121
- }
122
- atCommandStart = false;
123
- if (!isCommandPos)
124
- continue;
125
- for (const kw of ["sudoedit", "visudo", "sudo", "doas", "pkexec"]) {
126
- if (command.startsWith(kw, i)) {
127
- const next = command[i + kw.length] ?? "";
128
- if (!/[-\/.\w]/.test(next)) {
129
- hits.push({ index: i, kw });
130
- i += kw.length - 1;
131
- break;
132
- }
133
- }
134
- }
135
- }
136
- return hits;
137
- }
6
+ const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
138
7
  /**
139
- * First option token after a privilege keyword that pkexec does not
140
- * accept, or null when the command's options are pkexec-compatible.
141
- * Scanning stops at the program name (`--` also terminates options, as
142
- * in sudo); tokens after it belong to the program, not to pkexec.
8
+ * Redirects sudo/doas to pkexec without touching the command text.
9
+ *
10
+ * The shell tool spawns `$SHELL -c 'command'` with the environment built here,
11
+ * so putting the shims first on PATH lets the shell's own name resolution pick
12
+ * them up — `sudo`, `$(sudo …)`, `xargs sudo`, `FOO=1 sudo`, nested scripts.
13
+ * Nothing parses the command string, and nothing parses pkexec's output: the
14
+ * shims hand pkexec's exit status (126 dismissed, 127 not authorized) back to
15
+ * the caller unchanged.
143
16
  *
144
- * - `sudo -n true` → `-n` (pkexec has no short options)
145
- * - `sudo --non-interactive x` → `--non-interactive`
146
- * - `sudo --user root x` → null (compatible)
147
- * - `sudo -- x -n` → null (`--` ends sudo options)
17
+ * The shell is non-interactive and non-login, so no profile rewrites PATH and
18
+ * no alias can shadow the shims.
148
19
  */
149
- function findBadOption(command, hit) {
150
- let i = hit.index + hit.kw.length;
151
- while (i < command.length && /\s/.test(command[i]))
152
- i++;
153
- while (i < command.length && command[i] === "-") {
154
- let end = i + 1;
155
- while (end < command.length && !/\s/.test(command[end]) && !/[;|&()]/.test(command[end]))
156
- end++;
157
- const token = command.slice(i, end);
158
- if (token === "--")
159
- break;
160
- if (!token.startsWith("--"))
161
- return token;
162
- if (!PKEXEC_OPTIONS.has(token.split("=")[0]))
163
- return token;
164
- i = end;
165
- while (i < command.length && /\s/.test(command[i]))
166
- i++;
167
- }
168
- return null;
169
- }
170
- const translations = {
171
- en: {
172
- blocked: "Privilege escalation command blocked by plugin",
173
- polkitDenied: "Polkit authentication denied by user",
174
- badOption: 'Option "{opt}" is not supported by pkexec. Remove it or use a pkexec-compatible one (--user, --keep-cwd, --disable-internal-agent).',
175
- },
176
- zh: {
177
- blocked: "权限提升命令已被插件拦截",
178
- polkitDenied: "Polkit 认证已被用户拒绝",
179
- badOption: '选项 "{opt}" 不被 pkexec 支持,请移除或改用 pkexec 兼容选项(--user、--keep-cwd、--disable-internal-agent)。',
180
- },
181
- ja: {
182
- blocked: "特権昇格コマンドはプラグインによりブロックされました",
183
- polkitDenied: "Polkit 認証がユーザーによって拒否されました",
184
- badOption: 'オプション "{opt}" は pkexec ではサポートされていません。削除するか、pkexec 互換オプション(--user、--keep-cwd、--disable-internal-agent)を使用してください。',
185
- },
186
- ko: {
187
- blocked: "권한 상승 명령이 플러그인에 의해 차단되었습니다",
188
- polkitDenied: "Polkit 인증이 사용자에 의해 거부되었습니다",
189
- badOption: '옵션 "{opt}"은(는) pkexec에서 지원되지 않습니다. 제거하거나 pkexec 호환 옵션(--user, --keep-cwd, --disable-internal-agent)을 사용하세요.',
190
- },
191
- de: {
192
- blocked: "Berechtigungseskalation vom Plugin blockiert",
193
- polkitDenied: "Polkit-Authentifizierung vom Benutzer abgelehnt",
194
- badOption: 'Option "{opt}" wird von pkexec nicht unterstützt. Entfernen Sie sie oder verwenden Sie eine pkexec-kompatible Option (--user, --keep-cwd, --disable-internal-agent).',
195
- },
196
- fr: {
197
- blocked: "Commande d'élévation de privilèges bloquée par le plugin",
198
- polkitDenied: "Authentification polkit refusée par l'utilisateur",
199
- badOption: 'Option "{opt}" non prise en charge par pkexec. Supprimez-la ou utilisez une option compatible pkexec (--user, --keep-cwd, --disable-internal-agent).',
200
- },
201
- es: {
202
- blocked: "Comando de elevación de privilegios bloqueado por el plugin",
203
- polkitDenied: "Autenticación polkit denegada por el usuario",
204
- badOption: 'Opción "{opt}" no admitida por pkexec. Elimínela o use una opción compatible con pkexec (--user, --keep-cwd, --disable-internal-agent).',
205
- },
206
- pt: {
207
- blocked: "Comando de elevação de privilégios bloqueado pelo plugin",
208
- polkitDenied: "Autenticação polkit negada pelo usuário",
209
- badOption: 'Opção "{opt}" não suportada pelo pkexec. Remova-a ou use uma opção compatível com pkexec (--user, --keep-cwd, --disable-internal-agent).',
210
- },
211
- ru: {
212
- blocked: "Команда повышения привилегий заблокирована плагином",
213
- polkitDenied: "Аутентификация polkit отклонена пользователем",
214
- badOption: 'Параметр "{opt}" не поддерживается pkexec. Удалите его или используйте совместимый параметр (--user, --keep-cwd, --disable-internal-agent).',
215
- },
216
- tr: {
217
- blocked: "Yetki yükseltme komutu eklenti tarafından engellendi",
218
- polkitDenied: "Polkit kimlik doğrulaması kullanıcı tarafından reddedildi",
219
- badOption: '"{opt}" seçeneği pkexec tarafından desteklenmiyor. Kaldırın veya pkexec uyumlu bir seçenek kullanın (--user, --keep-cwd, --disable-internal-agent).',
220
- },
221
- uk: {
222
- blocked: "Команду підвищення привілеїв заблоковано плагіном",
223
- polkitDenied: "Автентифікацію polkit відхилено користувачем",
224
- badOption: 'Параметр "{opt}" не підтримується pkexec. Видаліть його або використайте сумісний параметр (--user, --keep-cwd, --disable-internal-agent).',
225
- },
226
- };
227
- function detectLocale() {
228
- const raw = process.env.LC_MESSAGES || process.env.LANG || "en";
229
- const tag = raw.split(".")[0].replace(/_/g, "-").toLowerCase();
230
- for (const prefix of ["zh", "ja", "ko", "de", "fr", "es", "pt", "ru", "tr", "uk"]) {
231
- if (tag.startsWith(prefix))
232
- return prefix;
233
- }
234
- return "en";
235
- }
236
- const MSG = translations[detectLocale()] ?? translations.en;
237
- export const PolkitPlugin = async (_input) => {
238
- const deniedCommands = new Set();
239
- return {
240
- "tool.execute.before": async (hookInput, hookOutput) => {
241
- if (hookInput.tool !== "bash")
242
- return;
243
- const command = hookOutput?.args?.command ?? "";
244
- const hits = scanPrivileged(command);
245
- if (hits.length === 0)
246
- return;
247
- if (deniedCommands.has(command)) {
248
- throw new Error(MSG.polkitDenied);
249
- }
250
- if (hits.some((h) => h.kw === "sudoedit" || h.kw === "visudo")) {
251
- throw new Error(MSG.blocked);
252
- }
253
- // Rewrite would produce `pkexec <bad option>`: fail with a clear
254
- // message instead of a confusing `Cannot run program` error.
255
- for (const h of hits) {
256
- if (h.kw === "sudo" || h.kw === "doas" || h.kw === "pkexec") {
257
- const bad = findBadOption(command, h);
258
- if (bad)
259
- throw new Error(MSG.badOption.replace("{opt}", bad));
260
- }
261
- }
262
- // Already pkexec (leading or mid-command): pass through untouched.
263
- if (hits.every((h) => h.kw === "pkexec"))
264
- return;
265
- // Rewrite every in-command sudo/doas to pkexec (back to front so
266
- // indices stay valid): `cat x | sudo tee y` -> `cat x | pkexec tee y`.
267
- // No other rewriting: the command keeps its exact shape so the agent
268
- // sees only the minimal change (sudo -> pkexec).
269
- let rewritten = command;
270
- for (const h of [...hits].reverse()) {
271
- if (h.kw === "sudo" || h.kw === "doas") {
272
- rewritten =
273
- rewritten.slice(0, h.index) + "pkexec" + rewritten.slice(h.index + h.kw.length);
274
- }
275
- }
276
- hookOutput.args.command = rewritten;
277
- },
278
- "tool.execute.after": async (hookInput, hookOutput) => {
279
- if (hookInput.tool !== "bash")
280
- return;
281
- const command = hookInput.args?.command ?? "";
282
- const hits = scanPrivileged(command);
283
- if (hits.length === 0)
284
- return;
285
- // Reconstruct the user's original command for the deny list: map
286
- // pkexec back to sudo.
287
- let original = command;
288
- for (const h of [...scanPrivileged(original)].reverse()) {
289
- if (h.kw === "pkexec") {
290
- original = original.slice(0, h.index) + "sudo" + original.slice(h.index + 6);
291
- }
292
- }
293
- // Only unambiguous authentication failures are reported and deny
294
- // listed. A hang (dialog never appears) is bounded by the bash tool's
295
- // own timeout (default 2 min) and is NOT translated here: it cannot
296
- // be told apart from a long-running command.
297
- if (/\bNot authorized\b/.test(hookOutput.output) ||
298
- /\bError executing command as another user\b/.test(hookOutput.output) ||
299
- /\bError creating textual authentication agent\b/.test(hookOutput.output)) {
300
- deniedCommands.add(original);
301
- throw new Error(MSG.polkitDenied);
302
- }
303
- },
304
- };
20
+ const setup = async (ctx) => {
21
+ const shimDir = installPolkitShims();
22
+ const prefix = `${shimDir}:`;
23
+ // Hooks stack across plugin loads; prepending once keeps PATH duplicate-free.
24
+ await ctx.shell.hook("create.before", (event) => {
25
+ const path = event.env.PATH || FALLBACK_PATH;
26
+ if (!path.startsWith(prefix))
27
+ event.env.PATH = prefix + path;
28
+ });
29
+ return () => removePolkitShims(shimDir);
305
30
  };
31
+ /**
32
+ * OpenCode 2.0 requires a default export shaped `{ id, setup }`. Type-only
33
+ * imports keep the built plugin free of runtime dependencies.
34
+ */
35
+ export default { id: "opencode-polkit", setup };
36
+ //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAG,8DAA8D,CAAA;AAE5E,MAAM,aAAa,GAAG;IACpB,mCAAmC;IACnC,qCAAqC;IACrC,sBAAsB;IACtB,aAAa;IACb,aAAa;IACb,kBAAkB;CACnB,CAAA;AASD,MAAM,YAAY,GAA6B;IAC7C,EAAE,EAAE;QACF,OAAO,EAAE,gDAAgD;QACzD,QAAQ,EAAE,qEAAqE;QAC/E,cAAc,EAAE,8CAA8C;QAC9D,YAAY,EAAE,sCAAsC;KACrD;IACD,EAAE,EAAE;QACF,OAAO,EAAE,cAAc;QACvB,QAAQ,EAAE,yCAAyC;QACnD,cAAc,EAAE,8BAA8B;QAC9C,YAAY,EAAE,iBAAiB;KAChC;IACD,EAAE,EAAE;QACF,OAAO,EAAE,4BAA4B;QACrC,QAAQ,EAAE,wDAAwD;QAClE,cAAc,EAAE,uCAAuC;QACvD,YAAY,EAAE,2BAA2B;KAC1C;IACD,EAAE,EAAE;QACF,OAAO,EAAE,4BAA4B;QACrC,QAAQ,EAAE,wDAAwD;QAClE,cAAc,EAAE,uCAAuC;QACvD,YAAY,EAAE,4BAA4B;KAC3C;IACD,EAAE,EAAE;QACF,OAAO,EAAE,8CAA8C;QACvD,QAAQ,EAAE,kFAAkF;QAC5F,cAAc,EAAE,mDAAmD;QACnE,YAAY,EAAE,iDAAiD;KAChE;IACD,EAAE,EAAE;QACF,OAAO,EAAE,0DAA0D;QACnE,QAAQ,EAAE,qEAAqE;QAC/E,cAAc,EAAE,kDAAkD;QAClE,YAAY,EAAE,mDAAmD;KAClE;IACD,EAAE,EAAE;QACF,OAAO,EAAE,6DAA6D;QACtE,QAAQ,EAAE,sFAAsF;QAChG,cAAc,EAAE,mDAAmD;QACnE,YAAY,EAAE,8CAA8C;KAC7D;IACD,EAAE,EAAE;QACF,OAAO,EAAE,0DAA0D;QACnE,QAAQ,EAAE,0FAA0F;QACpG,cAAc,EAAE,qDAAqD;QACrE,YAAY,EAAE,yCAAyC;KACxD;IACD,EAAE,EAAE;QACF,OAAO,EAAE,qDAAqD;QAC9D,QAAQ,EAAE,6FAA6F;QACvG,cAAc,EAAE,8CAA8C;QAC9D,YAAY,EAAE,+CAA+C;KAC9D;IACD,EAAE,EAAE;QACF,OAAO,EAAE,sDAAsD;QAC/D,QAAQ,EAAE,uFAAuF;QACjG,cAAc,EAAE,mDAAmD;QACnE,YAAY,EAAE,2DAA2D;KAC1E;IACD,EAAE,EAAE;QACF,OAAO,EAAE,mDAAmD;QAC5D,QAAQ,EAAE,yFAAyF;QACnG,cAAc,EAAE,+CAA+C;QAC/D,YAAY,EAAE,8CAA8C;KAC7D;CACF,CAAA;AAED,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAA;IAC/D,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;IAC9D,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAClF,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,MAAM,CAAA;IAC3C,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,GAAG,GAAa,YAAY,CAAC,YAAY,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,CAAA;AAErE,SAAS,eAAe;IACtB,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE;gBACvD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;aAClC,CAAC,CAAA;YACF,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;gBACrF,OAAO,IAAI,CAAA;YACb,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,EAAE;YAC5D,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAA;QACF,OAAO,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,MAAM,SAAS,GAAG,eAAe,EAAE,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAW,KAAK,EAAE,MAAmB,EAAE,EAAE;IAChE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAA;IAExC,OAAO;QACL,qBAAqB,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;YACrD,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM;gBAAE,OAAM;YACrC,MAAM,OAAO,GAAW,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,EAAE,CAAA;YACvD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,OAAM;YAEhC,IAAI,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACnC,CAAC;YAED,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,SAAS;oBAAE,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;gBACnD,OAAM;YACR,CAAC;YAED,IAAI,4CAA4C,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/D,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC9B,CAAC;YAED,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAC/B,CAAC;YAED,UAAU,CAAC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CACvC,sCAAsC,EACtC,QAAQ,CACT,CAAA;QACH,CAAC;QACD,oBAAoB,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;YACpD,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM;gBAAE,OAAM;YACrC,MAAM,OAAO,GAAW,SAAS,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAA;YACrD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,OAAM;YAEhC,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CACrC,iCAAiC,EACjC,MAAM,CACP,CAAA;YAED,IAAI,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;gBAC5C,6CAA6C,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1E,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;gBACnC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACnC,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC,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;;;;;;;;;;;;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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Exit status every shim uses for an invocation pkexec cannot express
3
+ * (EX_USAGE). Distinct from pkexec's own 126 (dismissed) and 127 (not
4
+ * authorized or failed) so callers can tell the two apart without reading
5
+ * any output.
6
+ */
7
+ export declare const SHIM_USAGE_EXIT = 64;
8
+ /**
9
+ * Writes the shim scripts into a fresh private directory and returns it.
10
+ * The caller prepends this directory to PATH; the shell then resolves
11
+ * `sudo`/`doas` to these files instead of the system ones. The directory is
12
+ * 0700 so no other local user can rewrite the scripts.
13
+ */
14
+ export declare function installPolkitShims(): string;
15
+ /** Deletes a directory created by installPolkitShims. */
16
+ export declare function removePolkitShims(directory: string): void;
package/dist/shims.js ADDED
@@ -0,0 +1,218 @@
1
+ import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { isAbsolute, join } from "node:path";
4
+ /**
5
+ * Exit status every shim uses for an invocation pkexec cannot express
6
+ * (EX_USAGE). Distinct from pkexec's own 126 (dismissed) and 127 (not
7
+ * authorized or failed) so callers can tell the two apart without reading
8
+ * any output.
9
+ */
10
+ export const SHIM_USAGE_EXIT = 64;
11
+ /**
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.
19
+ */
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() {
25
+ printf 'opencode-polkit: %s\\n' "$1" >&2
26
+ exit ${SHIM_USAGE_EXIT}
27
+ }
28
+
29
+ command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
30
+
31
+ user=
32
+ while [ $# -gt 0 ]; do
33
+ case $1 in
34
+ --)
35
+ shift
36
+ break
37
+ ;;
38
+ -u)
39
+ [ $# -ge 2 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
40
+ user=$2
41
+ shift 2
42
+ ;;
43
+ --user)
44
+ [ $# -ge 2 ] || fail 'sudo --user needs a username: --user USER PROGRAM'
45
+ user=$2
46
+ shift 2
47
+ ;;
48
+ -u?*)
49
+ user=\${1#-u}
50
+ [ -n "$user" ] || fail 'sudo -u needs a username: -u USER PROGRAM'
51
+ shift
52
+ ;;
53
+ --user=*)
54
+ user=\${1#--user=}
55
+ [ -n "$user" ] || fail 'sudo --user needs a username: --user USER PROGRAM'
56
+ shift
57
+ ;;
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.'
60
+ ;;
61
+ -v | --validate | -k | --reset-timestamp | -K | --remove-timestamp)
62
+ fail 'sudo credential caching does not exist under polkit; drop -v/-k.'
63
+ ;;
64
+ -l | --list)
65
+ fail 'sudo -l is not available under polkit; inspect policy with pkaction --verbose.'
66
+ ;;
67
+ -e | --edit)
68
+ fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
69
+ ;;
70
+ -i | --login | -s | --shell)
71
+ fail 'sudo -i/-s opens a login shell; run pkexec bash instead.'
72
+ ;;
73
+ -S | --stdin)
74
+ fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
75
+ ;;
76
+ -A | --askpass)
77
+ fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
78
+ ;;
79
+ -b | --background)
80
+ fail 'sudo -b backgrounds the command; append & instead.'
81
+ ;;
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.'
84
+ ;;
85
+ -?*)
86
+ fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
87
+ ;;
88
+ *)
89
+ break
90
+ ;;
91
+ esac
92
+ done
93
+
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 "$@"
98
+ status=$?
99
+ if [ $status -eq 126 ]; then
100
+ printf 'opencode-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
101
+ fi
102
+ exit $status
103
+ }
104
+
105
+ if [ -n "$user" ]; then
106
+ run_pkexec --user "$user" "$@"
107
+ fi
108
+ run_pkexec "$@"
109
+ `;
110
+ /** The doas shim: same contract, doas option grammar. */
111
+ const DOAS_SHIM = `#!/bin/sh
112
+ # opencode-polkit: doas shim. Routes doas to pkexec so polkit can authorize it.
113
+ # Generated at plugin startup; do not edit.
114
+
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
+
122
+ user=
123
+ while [ $# -gt 0 ]; do
124
+ case $1 in
125
+ --)
126
+ shift
127
+ break
128
+ ;;
129
+ -u)
130
+ [ $# -ge 2 ] || fail 'doas -u needs a username: -u USER PROGRAM'
131
+ user=$2
132
+ shift 2
133
+ ;;
134
+ -u?*)
135
+ user=\${1#-u}
136
+ [ -n "$user" ] || fail 'doas -u needs a username: -u USER PROGRAM'
137
+ shift
138
+ ;;
139
+ -n)
140
+ fail 'doas -n is not supported: pkexec has no non-interactive mode. Drop -n.'
141
+ ;;
142
+ -s)
143
+ fail 'doas -s opens a shell; run pkexec bash instead.'
144
+ ;;
145
+ -C | -a | -L)
146
+ fail "option has no pkexec equivalent: $1"
147
+ ;;
148
+ -?*)
149
+ fail "unknown doas option: $1"
150
+ ;;
151
+ *)
152
+ break
153
+ ;;
154
+ esac
155
+ done
156
+
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
+ }
167
+
168
+ if [ -n "$user" ]; then
169
+ run_pkexec --user "$user" "$@"
170
+ fi
171
+ run_pkexec "$@"
172
+ `;
173
+ /** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
174
+ const SUDOEDIT_SHIM = `#!/bin/sh
175
+ printf 'opencode-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
176
+ exit ${SHIM_USAGE_EXIT}
177
+ `;
178
+ /** visudo needs an interactive editor on a terminal the shell tool does not have. */
179
+ const VISUDO_SHIM = `#!/bin/sh
180
+ printf 'opencode-polkit: visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.\\n' >&2
181
+ exit ${SHIM_USAGE_EXIT}
182
+ `;
183
+ /** File name → script body. Names must match the commands being redirected. */
184
+ const POLKIT_SHIM_SCRIPTS = {
185
+ sudo: SUDO_SHIM,
186
+ doas: DOAS_SHIM,
187
+ sudoedit: SUDOEDIT_SHIM,
188
+ visudo: VISUDO_SHIM,
189
+ };
190
+ /**
191
+ * Writes the shim scripts into a fresh private directory and returns it.
192
+ * The caller prepends this directory to PATH; the shell then resolves
193
+ * `sudo`/`doas` to these files instead of the system ones. The directory is
194
+ * 0700 so no other local user can rewrite the scripts.
195
+ */
196
+ export function installPolkitShims() {
197
+ const directory = mkdtempSync(join(shimBaseDirectory(), "opencode-polkit-"));
198
+ for (const [name, script] of Object.entries(POLKIT_SHIM_SCRIPTS)) {
199
+ const file = join(directory, name);
200
+ writeFileSync(file, script);
201
+ // writeFileSync's mode is filtered by umask, so set it explicitly.
202
+ chmodSync(file, 0o755);
203
+ }
204
+ return directory;
205
+ }
206
+ /**
207
+ * Where shim directories live. Prefers the login session's runtime directory,
208
+ * which the system clears on logout, over /tmp which keeps leftovers.
209
+ */
210
+ function shimBaseDirectory() {
211
+ const runtime = process.env.XDG_RUNTIME_DIR;
212
+ return runtime && isAbsolute(runtime) && existsSync(runtime) ? runtime : tmpdir();
213
+ }
214
+ /** Deletes a directory created by installPolkitShims. */
215
+ export function removePolkitShims(directory) {
216
+ rmSync(directory, { recursive: true, force: true });
217
+ }
218
+ //# sourceMappingURL=shims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shims.js","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFtB,CAAA;AAED,yDAAyD;AACzD,MAAM,SAAS,GAAG;;;;;;QAMV,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDtB,CAAA;AAED,8EAA8E;AAC9E,MAAM,aAAa,GAAG;;OAEf,eAAe;CACrB,CAAA;AAED,qFAAqF;AACrF,MAAM,WAAW,GAAG;;OAEb,eAAe;CACrB,CAAA;AAED,+EAA+E;AAC/E,MAAM,mBAAmB,GAA2B;IAClD,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;CACpB,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAC5E,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;QAClC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC3B,mEAAmE;QACnE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACxB,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;IAC3C,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;AACnF,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACrD,CAAC"}
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Local-directory entrypoint for OpenCode: OpenCode 2.2 also resolves a
2
+ // directory through a named package.json and its "exports", but older 2.0
3
+ // releases only look for `server`/`index` at the root. npm installs reach the
4
+ // real build through package.json "exports"; this shim keeps
5
+ // `"plugins": ["/path/to/opencode-polkit"]` working on both.
6
+ export { default } from "./dist/index.js"
package/package.json CHANGED
@@ -1,37 +1,44 @@
1
1
  {
2
2
  "name": "opencode-polkit",
3
- "version": "0.1.5",
4
- "description": "Redirect sudo/doas to pkexec in OpenCode — uses system polkit agent for native auth dialogs",
3
+ "version": "0.3.0",
4
+ "description": "Redirect sudo/doas to pkexec in OpenCode via PATH shims, so the system polkit agent handles authentication",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
12
  }
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "index.js",
16
17
  "README.md",
18
+ "CHANGELOG.md",
17
19
  "LICENSE"
18
20
  ],
19
21
  "scripts": {
20
- "build": "deno task build",
21
- "check": "deno task check"
22
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
23
+ "check": "tsc -p tsconfig.json --noEmit"
22
24
  },
23
25
  "engines": {
24
- "opencode": ">=1.0.0"
26
+ "opencode": ">=2.0.29"
25
27
  },
26
28
  "peerDependencies": {
27
- "@opencode-ai/plugin": ">=1.0.0"
29
+ "@opencode/plugin": ">=2.0.29"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@opencode/plugin": {
33
+ "optional": true
34
+ }
28
35
  },
29
36
  "devDependencies": {
30
- "@opencode-ai/plugin": "^1.0.0",
31
- "bun-types": "^1.2.0",
37
+ "@opencode/plugin": "^2.2.5",
38
+ "@types/node": "^24.0.0",
32
39
  "typescript": "^5.5.0"
33
40
  },
34
- "keywords": ["opencode-plugin", "sudo", "pkexec", "polkit", "security"],
41
+ "keywords": ["opencode-plugin", "sudo", "doas", "pkexec", "polkit", "security"],
35
42
  "license": "MIT",
36
43
  "repository": {
37
44
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAA;AA2H9D,eAAO,MAAM,YAAY,EAAE,MAgD1B,CAAA"}