fullcourtdefense-cli 1.35.0 → 1.35.2
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/dist/actionPolicyEngine.js +59 -9
- package/dist/approvalPrompt.d.ts +25 -0
- package/dist/approvalPrompt.js +51 -0
- package/dist/blockExplanation.js +4 -1
- package/dist/commands/deterministicGuard.js +124 -6
- package/dist/commands/hook.js +8 -0
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.d.ts +3 -1
- package/dist/devConfirm.js +176 -30
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/devConfirm.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare const MIN_HUMAN_ANSWER_MS = 1000;
|
|
|
13
13
|
export declare function looksLikeClientAutoAnswer(answer: 'accept' | 'decline' | 'timeout', latencyMs: number): boolean;
|
|
14
14
|
/**
|
|
15
15
|
* OS-owned confirmation dialog — the IDE-independent fallback for "Ask the developer".
|
|
16
|
-
*
|
|
16
|
+
* Branded WinForms card on Windows, WKWebView card on macOS, `zenity` elsewhere.
|
|
17
17
|
* The agent cannot answer it: it is not connected to hook stdin, the MCP stream or the
|
|
18
18
|
* agent's terminal. Resolves `true` only for an explicit "Allow once"; decline, close,
|
|
19
19
|
* timeout, a missing dialog binary or no human present all resolve `false`.
|
|
@@ -42,6 +42,8 @@ export interface NativeDialogOptions {
|
|
|
42
42
|
alwaysScopeLabel?: string;
|
|
43
43
|
/** How long / how many times it would hold, e.g. "30 days" or "7 days or 20 uses". */
|
|
44
44
|
alwaysLimitsLabel?: string;
|
|
45
|
+
/** Icon and copy. Buttons stay Deny / Allow once / Always allow for `ask`. */
|
|
46
|
+
tone?: 'ask' | 'approval' | 'block';
|
|
45
47
|
}
|
|
46
48
|
export declare function confirmNativeActionDetailed(reason: string, timeoutMs?: number, options?: NativeDialogOptions): Promise<{
|
|
47
49
|
outcome: NativeDialogOutcome;
|
package/dist/devConfirm.js
CHANGED
|
@@ -107,7 +107,7 @@ function looksLikeClientAutoAnswer(answer, latencyMs) {
|
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* OS-owned confirmation dialog — the IDE-independent fallback for "Ask the developer".
|
|
110
|
-
*
|
|
110
|
+
* Branded WinForms card on Windows, WKWebView card on macOS, `zenity` elsewhere.
|
|
111
111
|
* The agent cannot answer it: it is not connected to hook stdin, the MCP stream or the
|
|
112
112
|
* agent's terminal. Resolves `true` only for an explicit "Allow once"; decline, close,
|
|
113
113
|
* timeout, a missing dialog binary or no human present all resolve `false`.
|
|
@@ -129,43 +129,158 @@ function nativeDialogAvailable() {
|
|
|
129
129
|
return true;
|
|
130
130
|
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
131
131
|
}
|
|
132
|
+
function b64(value) {
|
|
133
|
+
return Buffer.from(value, 'utf8').toString('base64');
|
|
134
|
+
}
|
|
135
|
+
function htmlEsc(value) {
|
|
136
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
137
|
+
}
|
|
138
|
+
function toneTheme(tone) {
|
|
139
|
+
if (tone === 'block')
|
|
140
|
+
return { icon: '#DC2626', badgeBg: '#FEF2F2', badgeFg: '#B91C1C', glyph: '✕' };
|
|
141
|
+
if (tone === 'approval')
|
|
142
|
+
return { icon: '#D97706', badgeBg: '#FFEDD5', badgeFg: '#C2410C', glyph: '!' };
|
|
143
|
+
return { icon: '#2563EB', badgeBg: '#DBEAFE', badgeFg: '#1D4ED8', glyph: '?' };
|
|
144
|
+
}
|
|
145
|
+
function argb(hex) {
|
|
146
|
+
const n = hex.replace('#', '');
|
|
147
|
+
return `${parseInt(n.slice(0, 2), 16)},${parseInt(n.slice(2, 4), 16)},${parseInt(n.slice(4, 6), 16)}`;
|
|
148
|
+
}
|
|
149
|
+
/** Card HTML for macOS WKWebView. Button labels stay the existing Deny / Allow once / Always allow. */
|
|
150
|
+
function dialogCardHtml(view, options) {
|
|
151
|
+
const theme = toneTheme(view.tone);
|
|
152
|
+
const bullets = (items, color) => items.map(item => `<li><span class="dot" style="background:${color}"></span>${htmlEsc(item)}</li>`).join('');
|
|
153
|
+
const always = options.allowAlways
|
|
154
|
+
? `<button class="ghost" onclick="location.hash='fcd-always'">${htmlEsc(exports.DIALOG_BUTTON_ALWAYS)}</button>`
|
|
155
|
+
: '';
|
|
156
|
+
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
157
|
+
*{box-sizing:border-box}body{margin:0;font:13px/1.45 -apple-system,Segoe UI,sans-serif;background:#f4f7fb;color:#0f172a}
|
|
158
|
+
.card{margin:12px;background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px 22px 16px;box-shadow:0 10px 30px rgba(15,23,42,.08)}
|
|
159
|
+
.brand{display:flex;justify-content:space-between;align-items:center;color:#64748b;font-weight:600;font-size:12px;margin-bottom:14px}
|
|
160
|
+
.hero{display:flex;gap:14px;align-items:flex-start}
|
|
161
|
+
.icon{width:44px;height:44px;border-radius:50%;background:${theme.badgeBg};color:${theme.icon};border:1px solid ${theme.icon}33;display:flex;align-items:center;justify-content:center;font:700 20px/1 sans-serif;flex:none}
|
|
162
|
+
.badge{display:inline-block;background:${theme.badgeBg};color:${theme.badgeFg};font-size:11px;font-weight:700;padding:3px 8px;border-radius:999px;margin-bottom:6px}
|
|
163
|
+
h1{margin:0 0 4px;font-size:22px}.sub{color:#64748b;margin:0 0 14px}
|
|
164
|
+
.box{border:1px solid #e2e8f0;border-radius:12px;padding:12px 14px;margin:0 0 10px;background:#f8fafc}
|
|
165
|
+
h2{margin:0 0 6px;font-size:13px}.k{color:#64748b;font-size:11px;font-weight:700}
|
|
166
|
+
.action{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:8px 10px}
|
|
167
|
+
ul{list-style:none;margin:0;padding:0}li{display:flex;gap:8px;margin:6px 0}
|
|
168
|
+
.dot{width:8px;height:8px;border-radius:50%;margin-top:6px;flex:none}
|
|
169
|
+
.row{display:flex;justify-content:flex-end;gap:8px;align-items:center;margin-top:14px;flex-wrap:wrap}
|
|
170
|
+
button{font:600 13px/1 -apple-system,Segoe UI,sans-serif;border-radius:8px;padding:9px 14px;cursor:pointer}
|
|
171
|
+
.primary{background:#1d4ed8;color:#fff;border:0}.ghost{background:#fff;border:1px solid #cbd5e1}
|
|
172
|
+
.danger{background:#fff;color:#dc2626;border:1px solid #fecaca}
|
|
173
|
+
.foot{color:#94a3b8;font-size:11px;margin-top:10px}
|
|
174
|
+
</style></head><body><div class="card">
|
|
175
|
+
<div class="brand"><span>FullCourtDefense</span><span>Keep developers moving, safely.</span></div>
|
|
176
|
+
<div class="hero"><div class="icon">${theme.glyph}</div><div>
|
|
177
|
+
<span class="badge">${htmlEsc(view.badge)}</span>
|
|
178
|
+
<h1>${htmlEsc(view.headline)}</h1>
|
|
179
|
+
<p class="sub">${htmlEsc(view.subtitle)}</p>
|
|
180
|
+
</div></div>
|
|
181
|
+
<div class="box"><h2>Action</h2>
|
|
182
|
+
${view.kind ? `<div class="k">${htmlEsc(view.kind)}</div>` : ''}
|
|
183
|
+
${view.action ? `<div class="action">${htmlEsc(view.action)}</div>` : '<p class="sub">The action is described in the IDE message.</p>'}
|
|
184
|
+
</div>
|
|
185
|
+
${view.policy ? `<div class="box"><h2>Matched policy</h2><div><span class="k">Policy</span> ${htmlEsc(view.policy)}</div>${view.scope ? `<div><span class="k">Scope</span> ${htmlEsc(view.scope)}</div>` : ''}</div>` : ''}
|
|
186
|
+
<h2>Why it triggered</h2><ul>${bullets(view.why, theme.icon)}</ul>
|
|
187
|
+
<h2>What happens next</h2><ul>${bullets(view.next, '#2563EB')}</ul>
|
|
188
|
+
<div class="row">${always}
|
|
189
|
+
<button class="primary" onclick="location.hash='fcd-once'">${htmlEsc(exports.DIALOG_BUTTON_ONCE)}</button>
|
|
190
|
+
<button class="danger" onclick="location.hash='fcd-deny'">${htmlEsc(exports.DIALOG_BUTTON_DENY)}</button>
|
|
191
|
+
</div>
|
|
192
|
+
<p class="foot">${htmlEsc(view.footer)}</p>
|
|
193
|
+
</div></body></html>`;
|
|
194
|
+
}
|
|
132
195
|
/**
|
|
133
|
-
* Windows
|
|
134
|
-
*
|
|
135
|
-
* TopMost keeps it above the IDE that triggered it; Enter/Escape both mean Deny.
|
|
196
|
+
* Windows branded card. Compact prompt text stays the first payload so tests still
|
|
197
|
+
* read the dialog copy; layout facts travel as JSON.
|
|
136
198
|
* Exit codes: 0 = Allow once, 3 = Always allow, 2 = timed out, anything else = Deny.
|
|
137
199
|
*/
|
|
138
|
-
function windowsDialogScript(message, timeoutSeconds, options, title
|
|
139
|
-
const encoded =
|
|
140
|
-
const encodedTitle =
|
|
200
|
+
function windowsDialogScript(message, timeoutSeconds, options, title, view) {
|
|
201
|
+
const encoded = b64(message);
|
|
202
|
+
const encodedTitle = b64(title);
|
|
203
|
+
const encodedView = b64(JSON.stringify(view));
|
|
204
|
+
const theme = toneTheme(view.tone);
|
|
141
205
|
return [
|
|
142
206
|
`# ${exports.DIALOG_SCRIPT_MARKER}`,
|
|
143
207
|
'Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing',
|
|
144
208
|
'[System.Windows.Forms.Application]::EnableVisualStyles()',
|
|
145
209
|
`$msg=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,
|
|
146
210
|
`$title=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedTitle}'))`,
|
|
147
|
-
|
|
148
|
-
"$
|
|
149
|
-
|
|
150
|
-
"$
|
|
151
|
-
|
|
152
|
-
"$
|
|
153
|
-
|
|
154
|
-
|
|
211
|
+
`$v=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedView}')) | ConvertFrom-Json`,
|
|
212
|
+
"$bg=[Drawing.Color]::White; $muted=[Drawing.Color]::FromArgb(100,116,139); $ink=[Drawing.Color]::FromArgb(15,23,42)",
|
|
213
|
+
`$accent=[Drawing.Color]::FromArgb(${argb(theme.icon)}); $badgeBg=[Drawing.Color]::FromArgb(${argb(theme.badgeBg)}); $badgeFg=[Drawing.Color]::FromArgb(${argb(theme.badgeFg)})`,
|
|
214
|
+
"$f=New-Object Windows.Forms.Form; $f.Text=$title; $f.TopMost=$true; $f.StartPosition='CenterScreen'; $f.FormBorderStyle='FixedDialog'; $f.MaximizeBox=$false; $f.MinimizeBox=$false; $f.ShowInTaskbar=$true; $f.BackColor=$bg; $f.Font=New-Object Drawing.Font('Segoe UI',9.75); $f.ClientSize=New-Object Drawing.Size(700,640)",
|
|
215
|
+
// Panel.Padding does not move absolutely positioned children: every control takes $x0 explicitly.
|
|
216
|
+
"$root=New-Object Windows.Forms.Panel; $root.Dock='Fill'; $root.AutoScroll=$true; $root.BackColor=$bg; $f.Controls.Add($root)",
|
|
217
|
+
"function AddLabel($parent,$text,$font,$color,$y,$w,$x){ $l=New-Object Windows.Forms.Label; $l.Text=$text; $l.Font=$font; $l.ForeColor=$color; $l.AutoSize=$true; $l.MaximumSize=New-Object Drawing.Size($w,0); $l.Location=New-Object Drawing.Point($(if($x){$x}else{$x0}),$y); $parent.Controls.Add($l); return $l }",
|
|
218
|
+
"$x0=24; $y=18; $w=652",
|
|
219
|
+
"AddLabel $root 'FullCourtDefense' (New-Object Drawing.Font('Segoe UI',9.75,[Drawing.FontStyle]::Bold)) $muted $y $w | Out-Null; $y+=30",
|
|
220
|
+
`$icon=New-Object Windows.Forms.Label; $icon.Text='${theme.glyph}'; $icon.Font=New-Object Drawing.Font('Segoe UI',18,[Drawing.FontStyle]::Bold); $icon.TextAlign='MiddleCenter'; $icon.Size=New-Object Drawing.Size(44,44); $icon.Location=New-Object Drawing.Point($x0,$y); $icon.ForeColor=$accent; $icon.BackColor=$badgeBg; $root.Controls.Add($icon)`,
|
|
221
|
+
"$hx=$x0+58; $hw=$w-58",
|
|
222
|
+
"$badge=AddLabel $root ([string]$v.badge) (New-Object Drawing.Font('Segoe UI',8.25,[Drawing.FontStyle]::Bold)) $badgeFg $y $hw $hx; $badge.BackColor=$badgeBg; $badge.Padding=New-Object Windows.Forms.Padding(6,2,6,2)",
|
|
223
|
+
"$h=AddLabel $root ([string]$v.headline) (New-Object Drawing.Font('Segoe UI',16,[Drawing.FontStyle]::Bold)) $ink ($badge.Bottom+4) $hw $hx",
|
|
224
|
+
"$s=AddLabel $root ([string]$v.subtitle) (New-Object Drawing.Font('Segoe UI',9.75)) $muted ($h.Bottom+4) $hw $hx; $y=[Math]::Max($icon.Bottom,$s.Bottom)+18",
|
|
225
|
+
"AddLabel $root 'Action' (New-Object Drawing.Font('Segoe UI',9.75,[Drawing.FontStyle]::Bold)) $ink $y $w | Out-Null; $y+=22",
|
|
226
|
+
"if($v.kind){ AddLabel $root ([string]$v.kind) (New-Object Drawing.Font('Segoe UI',8.25,[Drawing.FontStyle]::Bold)) $muted $y $w | Out-Null; $y+=18 }",
|
|
227
|
+
"if($v.action){ $a=AddLabel $root ([string]$v.action) (New-Object Drawing.Font('Consolas',9.75)) $ink $y $w; $y=$a.Bottom+12 } else { $y+=8 }",
|
|
228
|
+
"if($v.policy){ AddLabel $root 'Matched policy' (New-Object Drawing.Font('Segoe UI',9.75,[Drawing.FontStyle]::Bold)) $ink $y $w | Out-Null; $y+=22; $pn=AddLabel $root ([string]$v.policy) (New-Object Drawing.Font('Segoe UI',9.75)) $ink $y $w; $y=$pn.Bottom+2; if($v.scope){ $ps=AddLabel $root ([string]$v.scope) (New-Object Drawing.Font('Segoe UI',9)) $muted $y $w; $y=$ps.Bottom+12 } }",
|
|
229
|
+
"AddLabel $root 'Why it triggered' (New-Object Drawing.Font('Segoe UI',9.75,[Drawing.FontStyle]::Bold)) $ink $y $w | Out-Null; $y+=22",
|
|
230
|
+
"foreach($item in @($v.why)){ $row=AddLabel $root ('• '+$item) (New-Object Drawing.Font('Segoe UI',9.75)) $ink $y $w; $y=$row.Bottom+2 }",
|
|
231
|
+
"$y+=10; AddLabel $root 'What happens next' (New-Object Drawing.Font('Segoe UI',9.75,[Drawing.FontStyle]::Bold)) $ink $y $w | Out-Null; $y+=22",
|
|
232
|
+
"foreach($item in @($v.next)){ $row=AddLabel $root ('• '+$item) (New-Object Drawing.Font('Segoe UI',9.75)) $ink $y $w; $y=$row.Bottom+2 }",
|
|
233
|
+
// Fixed-size row spanning the content width: RightToLeft flow puts Deny at the right edge,
|
|
234
|
+
// and a known height lets the footer and the form size be computed before the window exists.
|
|
235
|
+
"$y+=18; $b=New-Object Windows.Forms.FlowLayoutPanel; $b.FlowDirection='RightToLeft'; $b.AutoSize=$false; $b.WrapContents=$false; $b.Size=New-Object Drawing.Size($w,40); $b.Location=New-Object Drawing.Point($x0,$y)",
|
|
236
|
+
`$deny=New-Object Windows.Forms.Button; $deny.Text='${exports.DIALOG_BUTTON_DENY}'; $deny.AutoSize=$true; $deny.MinimumSize=New-Object Drawing.Size(96,34); $deny.FlatStyle='Flat'; $deny.ForeColor=[Drawing.Color]::FromArgb(220,38,38); $deny.BackColor=$bg; $deny.FlatAppearance.BorderColor=[Drawing.Color]::FromArgb(254,202,202); $deny.DialogResult='Cancel'; $deny.TabIndex=0; $b.Controls.Add($deny)`,
|
|
237
|
+
`$once=New-Object Windows.Forms.Button; $once.Text='${exports.DIALOG_BUTTON_ONCE}'; $once.AutoSize=$true; $once.MinimumSize=New-Object Drawing.Size(110,34); $once.FlatStyle='Flat'; $once.ForeColor=[Drawing.Color]::White; $once.BackColor=[Drawing.Color]::FromArgb(29,78,216); $once.FlatAppearance.BorderSize=0; $once.DialogResult='OK'; $once.TabIndex=1; $b.Controls.Add($once)`,
|
|
155
238
|
options.allowAlways
|
|
156
|
-
? `$always=New-Object Windows.Forms.Button; $always.Text='${exports.DIALOG_BUTTON_ALWAYS}'; $always.AutoSize=$true; $always.MinimumSize=New-Object Drawing.Size(110,
|
|
239
|
+
? `$always=New-Object Windows.Forms.Button; $always.Text='${exports.DIALOG_BUTTON_ALWAYS}'; $always.AutoSize=$true; $always.MinimumSize=New-Object Drawing.Size(110,34); $always.FlatStyle='Flat'; $always.BackColor=$bg; $always.DialogResult='Yes'; $always.TabIndex=2; $b.Controls.Add($always)`
|
|
157
240
|
: '',
|
|
158
|
-
"$
|
|
241
|
+
"$root.Controls.Add($b); $foot=AddLabel $root ([string]$v.footer) (New-Object Drawing.Font('Segoe UI',8.25)) $muted ($b.Bottom+14) $w",
|
|
242
|
+
// Fit the window to its content; only a very long action / reason list scrolls.
|
|
243
|
+
"$wa=[Windows.Forms.Screen]::PrimaryScreen.WorkingArea; $f.ClientSize=New-Object Drawing.Size(($w+2*$x0),[Math]::Min($foot.Bottom+18,[Math]::Max(360,$wa.Height-120)))",
|
|
244
|
+
"$f.CancelButton=$deny; $f.AcceptButton=$deny; $f.ActiveControl=$deny",
|
|
159
245
|
`$timer=New-Object Windows.Forms.Timer; $timer.Interval=${timeoutSeconds * 1000}; $timer.Add_Tick({ $f.DialogResult='Abort'; $f.Close() })`,
|
|
160
|
-
// The hook spawns PowerShell with a hidden startup show-state (no console flash);
|
|
161
|
-
// WinForms applies that state to the FIRST window, so the form would run
|
|
162
|
-
// invisibly. Bouncing WindowState issues a real ShowWindow(SW_RESTORE).
|
|
163
246
|
"$f.Add_Load({ $f.WindowState='Minimized'; $f.WindowState='Normal' })",
|
|
164
247
|
"$f.Add_Shown({ $f.Activate(); $deny.Select(); $deny.Focus(); $timer.Start() })",
|
|
165
248
|
"$r=$f.ShowDialog(); $timer.Stop()",
|
|
166
249
|
"if($r -eq 'OK'){exit 0} elseif($r -eq 'Yes'){exit 3} elseif($r -eq 'Abort'){exit 2} else {exit 1}",
|
|
167
250
|
].filter(Boolean).join('\n');
|
|
168
251
|
}
|
|
252
|
+
function macosDialogScript(html, timeoutSeconds, allowAlways) {
|
|
253
|
+
const encoded = b64(html);
|
|
254
|
+
return `// ${exports.DIALOG_SCRIPT_MARKER}
|
|
255
|
+
${allowAlways ? `// ${exports.DIALOG_BUTTON_ALWAYS}` : '// two-button'}
|
|
256
|
+
ObjC.import('Cocoa');
|
|
257
|
+
ObjC.import('WebKit');
|
|
258
|
+
var html = $.NSString.alloc.initWithDataEncoding($.NSData.alloc.initWithBase64Encoding('${encoded}'), $.NSUTF8StringEncoding);
|
|
259
|
+
var app = $.NSApplication.sharedApplication;
|
|
260
|
+
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
|
|
261
|
+
var win = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer($.NSMakeRect(0,0,720,640), (1<<0)|(1<<1)|(1<<3), 2, false);
|
|
262
|
+
win.setTitle('FullCourtDefense');
|
|
263
|
+
win.setBackgroundColor($.NSColor.whiteColor);
|
|
264
|
+
var web = $.WKWebView.alloc.initWithFrame($.NSMakeRect(0,0,720,640));
|
|
265
|
+
win.contentView.addSubview(web);
|
|
266
|
+
web.loadHTMLStringbaseURL(html, $.NSURL.URLWithString('about:blank'));
|
|
267
|
+
win.center();
|
|
268
|
+
win.makeKeyAndOrderFront(win);
|
|
269
|
+
app.activateIgnoringOtherApps(true);
|
|
270
|
+
var deadline = Date.now() + ${timeoutSeconds * 1000};
|
|
271
|
+
var result = 'timeout';
|
|
272
|
+
while (Date.now() < deadline) {
|
|
273
|
+
$.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.12));
|
|
274
|
+
var url = String(web.URL.absoluteString);
|
|
275
|
+
if (url.indexOf('fcd-once') >= 0) { result = '${exports.DIALOG_BUTTON_ONCE}'; break; }
|
|
276
|
+
if (url.indexOf('fcd-always') >= 0) { result = '${exports.DIALOG_BUTTON_ALWAYS}'; break; }
|
|
277
|
+
if (url.indexOf('fcd-deny') >= 0) { result = '${exports.DIALOG_BUTTON_DENY}'; break; }
|
|
278
|
+
if (!win.isVisible) { result = '${exports.DIALOG_BUTTON_DENY}'; break; }
|
|
279
|
+
}
|
|
280
|
+
win.close();
|
|
281
|
+
result;
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
169
284
|
async function confirmNativeActionDetailed(reason, timeoutMs = 60_000, options = {}) {
|
|
170
285
|
if (!nativeDialogAvailable())
|
|
171
286
|
return { outcome: 'unavailable', latencyMs: 0 };
|
|
@@ -173,21 +288,17 @@ async function confirmNativeActionDetailed(reason, timeoutMs = 60_000, options =
|
|
|
173
288
|
let executable;
|
|
174
289
|
let args;
|
|
175
290
|
const message = (0, approvalPrompt_1.nativeApprovalMessage)(reason, { allowAlways: options.allowAlways, alwaysScopeLabel: options.alwaysScopeLabel, alwaysLimitsLabel: options.alwaysLimitsLabel });
|
|
291
|
+
const view = (0, approvalPrompt_1.nativeDialogView)(reason, { allowAlways: options.allowAlways, alwaysScopeLabel: options.alwaysScopeLabel, alwaysLimitsLabel: options.alwaysLimitsLabel, tone: options.tone || 'ask' });
|
|
176
292
|
// The title names the kind of action and the policy, so the window itself says what is asking.
|
|
177
293
|
const title = (0, approvalPrompt_1.nativeApprovalTitle)(reason);
|
|
178
294
|
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
179
295
|
if (process.platform === 'win32') {
|
|
180
296
|
executable = 'powershell.exe';
|
|
181
|
-
args = ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(windowsDialogScript(message, timeoutSeconds, options, title), 'utf16le').toString('base64')];
|
|
297
|
+
args = ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(windowsDialogScript(message, timeoutSeconds, options, title, view), 'utf16le').toString('base64')];
|
|
182
298
|
}
|
|
183
299
|
else if (process.platform === 'darwin') {
|
|
184
300
|
executable = '/usr/bin/osascript';
|
|
185
|
-
|
|
186
|
-
? `{"${exports.DIALOG_BUTTON_DENY}", "${exports.DIALOG_BUTTON_ONCE}", "${exports.DIALOG_BUTTON_ALWAYS}"}`
|
|
187
|
-
: `{"${exports.DIALOG_BUTTON_DENY}", "${exports.DIALOG_BUTTON_ONCE}"}`;
|
|
188
|
-
// The pressed label is printed on stdout; Deny is the cancel button (exit 1).
|
|
189
|
-
// Message and title travel as argv items: never interpolated into the script.
|
|
190
|
-
args = ['-e', 'on run argv', '-e', `set r to display dialog (item 1 of argv) with title (item 2 of argv) buttons ${buttons} default button "${exports.DIALOG_BUTTON_DENY}" cancel button "${exports.DIALOG_BUTTON_DENY}" giving up after ${timeoutSeconds}`, '-e', 'if gave up of r then return "timeout"', '-e', 'return button returned of r', '-e', 'end run', message, title];
|
|
301
|
+
args = ['-l', 'JavaScript', '-e', macosDialogScript(dialogCardHtml(view, options), timeoutSeconds, Boolean(options.allowAlways))];
|
|
191
302
|
}
|
|
192
303
|
else {
|
|
193
304
|
executable = 'zenity';
|
|
@@ -660,6 +771,10 @@ function sweepExpiredConfirmations(now = new Date(), ttlMs = exports.CONFIRM_PEN
|
|
|
660
771
|
// ---------------------------------------------------------------------------
|
|
661
772
|
/** Shell words that run the program that follows them, not a program of their own. */
|
|
662
773
|
const SHELL_LEAD_WRAPPERS = new Set(['sudo', 'doas', 'env', 'nice', 'nohup', 'time', 'command', 'builtin', 'exec', 'xargs', 'timeout', 'stdbuf', 'chronic', 'busybox', 'call', 'start', '&']);
|
|
774
|
+
/** Control-flow words (sh / bash / PowerShell): structure, never a program of their own. */
|
|
775
|
+
const SHELL_CONTROL_WORDS = /^(if|then|else|elif|elseif|fi|do|done|for|while|until|case|esac|try|catch|finally|foreach|function|return|switch|break|continue)$/i;
|
|
776
|
+
/** Words a body follows directly (`then rm x`, `do echo`, PowerShell `else { … }`). */
|
|
777
|
+
const SHELL_BODY_LEADS = /^(then|do|else)$/i;
|
|
663
778
|
/** Cheap quote-aware split on `; && || | \n` — the grant needs program NAMES, not semantics. */
|
|
664
779
|
function splitShellSegments(command) {
|
|
665
780
|
const segments = [];
|
|
@@ -703,11 +818,20 @@ function splitShellSegments(command) {
|
|
|
703
818
|
function shellPrograms(command) {
|
|
704
819
|
const out = new Set();
|
|
705
820
|
for (const raw of splitShellSegments(command)) {
|
|
706
|
-
const words = raw.trim().
|
|
821
|
+
const words = raw.trim().split(/\s+/).filter(Boolean);
|
|
707
822
|
let i = 0;
|
|
708
|
-
// `$r = Invoke-WebRequest …` / `$r=Invoke-WebRequest …` / `FOO=bar cmd …`
|
|
823
|
+
// `$r = Invoke-WebRequest …` / `$r=Invoke-WebRequest …` / `FOO=bar cmd …` / `$t = (gcloud …)`
|
|
709
824
|
while (i < words.length) {
|
|
710
825
|
const w = words[i];
|
|
826
|
+
// Grouping before the program: `( gcloud`, `{ gcloud`, `(gcloud` — the program is inside.
|
|
827
|
+
if (/^[({]+$/.test(w)) {
|
|
828
|
+
i++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (/^[({]/.test(w)) {
|
|
832
|
+
words[i] = w.replace(/^[({]+/, '');
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
711
835
|
if (/^\$[\w:.]+$/.test(w) && words[i + 1] && /^[+-]?=$/.test(words[i + 1])) {
|
|
712
836
|
i += 2;
|
|
713
837
|
continue;
|
|
@@ -720,10 +844,32 @@ function shellPrograms(command) {
|
|
|
720
844
|
i++;
|
|
721
845
|
continue;
|
|
722
846
|
}
|
|
847
|
+
if (SHELL_BODY_LEADS.test(w)) {
|
|
848
|
+
i++;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (SHELL_CONTROL_WORDS.test(w)) {
|
|
852
|
+
// `if (cond) { body }` / `foreach ($x in $y) { body }` / `else { body }`: the program
|
|
853
|
+
// runs in the body, so skip to the first `{` and read on from there.
|
|
854
|
+
const open = words.findIndex((word, index) => index > i && word.includes('{'));
|
|
855
|
+
if (open < 0) {
|
|
856
|
+
i = words.length;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
const rest = words[open].slice(words[open].indexOf('{') + 1);
|
|
860
|
+
if (rest) {
|
|
861
|
+
words[open] = rest;
|
|
862
|
+
i = open;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
i = open + 1;
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
723
869
|
break;
|
|
724
870
|
}
|
|
725
871
|
const lead = words[i];
|
|
726
|
-
if (!lead || /^[$@(){}\]\[|&;<>#'"`-]/.test(lead) ||
|
|
872
|
+
if (!lead || /^[$@(){}\]\[|&;<>#'"`-]/.test(lead) || SHELL_CONTROL_WORDS.test(lead))
|
|
727
873
|
continue;
|
|
728
874
|
const name = lead.replace(/^["']|["']$/g, '').split(/[\\/]/).pop().replace(/\.(exe|cmd|bat|ps1|sh)$/i, '').toLowerCase();
|
|
729
875
|
if (name)
|
package/dist/version.json
CHANGED