feat: add permission consent prompts, drop permitted capability mask

This commit is contained in:
daniel committed 2026-09-30 08:32:48 +02:00
1 parent c54181103f
commit ae5e84592d
25 files changed
+1632 -94

No files matched your search

+140 -29
View File
@@ -8,6 +8,7 @@
#include <montauk/string.h>
#include <montauk/config.h>
#include <montauk/capabilities.h>
#include <montauk/consent.h>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
@@ -30,6 +31,7 @@ static constexpr int GRID_ROW_H = 22;
static constexpr int GRID_COL_W = 86;
static constexpr int MAX_GRANTS = 64;
static constexpr int MAX_ISSUES = 4;
static constexpr int MAX_DECISIONS = 64;
static constexpr int CAP_COUNT =
(int)(sizeof(montauk::caps::NAMES) / sizeof(montauk::caps::NAMES[0]));
@@ -37,12 +39,14 @@ static constexpr int CAP_COUNT =
enum Tab {
TAB_PROGRAMS = 0,
TAB_CAPS = 1,
TAB_COUNT = 2,
TAB_CONSENT = 2,
TAB_COUNT = 3,
};
static const char* const kTabLabels[TAB_COUNT] = {
"Programs",
"Capabilities",
"Consent",
};
// What each capability gates, in the order of montauk::caps::NAMES.
@@ -68,16 +72,15 @@ static const char* const kCapDescriptions[CAP_COUNT] = {
enum Field {
FIELD_EFFECTIVE = 0,
FIELD_DELEGABLE = 1,
FIELD_PERMITTED = 2,
FIELD_COUNT = 3,
FIELD_COUNT = 2,
};
static const char* const kFieldKeys[FIELD_COUNT] = {
"effective", "delegable", "permitted",
"effective", "delegable",
};
static const char* const kFieldLabels[FIELD_COUNT] = {
"Effective", "Delegable", "Permitted",
"Effective", "Delegable",
};
struct Grant {
@@ -104,6 +107,11 @@ static Color g_accent = colors::ACCENT;
static bool g_is_admin = false;
static int g_selected_grant = 0;
static int g_selected_cap = 0;
static int g_selected_decision = 0;
// Answers given to consent prompts (0:/config/consent.toml).
static montauk::consent::Decision g_decisions[MAX_DECISIONS];
static int g_decision_count = 0;
static int g_list_scroll = 0;
static bool g_list_sb_dragging = false;
static int g_list_sb_drag_offset = 0;
@@ -214,14 +222,6 @@ static void load_grants() {
for (int i = 0; i < g_grant_count; i++) {
Grant& g = g_grants[i];
uint64_t* m = g.mask;
// Same rule as caps::lookup(): an undeclared permitted set is exactly
// what the grant can use or pass on.
if (!g.declared[FIELD_PERMITTED] || m[FIELD_PERMITTED] == 0)
m[FIELD_PERMITTED] = m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE];
else if ((m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE]) & ~m[FIELD_PERMITTED])
add_issue(g, "Effective/delegable exceed permitted; the excess is dropped at launch");
if (g.path[0] == '\0') {
add_issue(g, "No path; this grant never applies");
@@ -231,6 +231,8 @@ static void load_grants() {
g.binary_present = bh >= 0;
if (bh >= 0) montauk::close(bh);
if (!g.binary_present) add_issue(g, "Binary not found on this system");
if (!montauk::caps::is_installed_path(g.path))
add_issue(g, "Never applies: not under 0:/os or 0:/apps, so it runs with no capabilities");
for (int j = 0; j < i; j++) {
if (montauk::streq(g_grants[j].path, g.path)) {
@@ -244,6 +246,25 @@ static void load_grants() {
if (g_selected_grant >= g_grant_count) g_selected_grant = 0;
}
static void load_decisions() {
g_decision_count = montauk::consent::load_all(g_decisions, MAX_DECISIONS);
if (g_selected_decision >= g_decision_count)
g_selected_decision = g_decision_count > 0 ? g_decision_count - 1 : 0;
}
static const char* base_name(const char* path) {
const char* base = path;
for (const char* p = path; *p; p++)
if (*p == '/' || *p == ':') base = p + 1;
return base;
}
static int cap_index(uint64_t bit) {
for (int i = 0; i < CAP_COUNT; i++)
if (montauk::caps::NAMES[i].bit == bit) return i;
return -1;
}
// ==== Layout ====
static mtk::Theme app_theme() {
@@ -271,7 +292,15 @@ static Rect list_rect() {
}
static int list_count() {
return g_tab == TAB_PROGRAMS ? g_grant_count : CAP_COUNT;
if (g_tab == TAB_PROGRAMS) return g_grant_count;
if (g_tab == TAB_CONSENT) return g_decision_count;
return CAP_COUNT;
}
static int* selected_for_tab() {
if (g_tab == TAB_PROGRAMS) return &g_selected_grant;
if (g_tab == TAB_CONSENT) return &g_selected_decision;
return &g_selected_cap;
}
static int list_visible_rows() {
@@ -322,6 +351,15 @@ static Rect reload_button() {
return {foot.x + foot.w - PAD - BUTTON_W, foot.y + 7, BUTTON_W, 30};
}
static Rect forget_button() {
Rect reload = reload_button();
return {reload.x - 8 - BUTTON_W, reload.y, BUTTON_W, reload.h};
}
static bool forget_visible() {
return g_is_admin && g_tab == TAB_CONSENT && g_decision_count > 0;
}
static bool mouse_in(const Rect& rect) {
return rect.contains(g_mouse_x, g_mouse_y);
}
@@ -382,17 +420,24 @@ static void draw_list(Canvas& c, const mtk::Theme& theme) {
int index = g_list_scroll + row;
if (index >= count) break;
Rect r = list_row_rect(row);
bool selected = index == (g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
bool selected = index == *selected_for_tab();
mtk::draw_list_row(c, r, selected, false, theme);
if (!selected && mouse_in(r))
c.fill_rounded_rect(r.x, r.y, r.w, r.h, theme.radius_md, theme.surface_hover);
const char* label = g_tab == TAB_PROGRAMS
? g_grants[index].id : montauk::caps::NAMES[index].name;
const char* label = g_tab == TAB_PROGRAMS ? g_grants[index].id
: g_tab == TAB_CONSENT ? base_name(g_decisions[index].path)
: montauk::caps::NAMES[index].name;
Color fg = selected ? theme.accent_fg : theme.text;
int ty = r.y + (r.h - fh) / 2;
draw_text_fit(c, r.x + 10, ty, label, r.w - 34, fg);
if (g_tab == TAB_CONSENT) {
Color dot = selected ? theme.accent_fg
: g_decisions[index].allow ? theme.accent : theme.text_muted;
fill_circle(c, r.x + r.w - 14, r.y + r.h / 2, 4, dot);
}
if (g_tab == TAB_PROGRAMS && g_grants[index].issue_count > 0) {
Color dot = selected ? theme.accent_fg : theme.danger;
fill_circle(c, r.x + r.w - 14, r.y + r.h / 2, 4, dot);
@@ -459,7 +504,6 @@ static void draw_programs_pane(Canvas& c, const mtk::Theme& theme) {
for (int f = 0; f < FIELD_COUNT; f++) {
const char* note = nullptr;
if (g.uses_all[f]) note = "(all)";
else if (f == FIELD_PERMITTED && !g.declared[f]) note = "(implied)";
if (!note) continue;
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
c.text(cx - text_width(note) / 2, y, note, theme.text_muted);
@@ -468,7 +512,7 @@ static void draw_programs_pane(Canvas& c, const mtk::Theme& theme) {
for (int i = 0; i < CAP_COUNT; i++) {
uint64_t bit = montauk::caps::NAMES[i].bit;
bool any = (g.mask[FIELD_PERMITTED] & bit) != 0;
bool any = ((g.mask[FIELD_EFFECTIVE] | g.mask[FIELD_DELEGABLE]) & bit) != 0;
if (i % 2 == 1)
c.fill_rect(x, y, w, GRID_ROW_H, theme.surface_alt);
c.text(x + 6, y + (GRID_ROW_H - fh) / 2, montauk::caps::NAMES[i].name,
@@ -541,6 +585,43 @@ static void draw_caps_pane(Canvas& c, const mtk::Theme& theme) {
"MAY PASS IT ON (DELEGABLE)", FIELD_DELEGABLE, cap.bit, theme);
}
static void draw_consent_pane(Canvas& c, const mtk::Theme& theme) {
int x = pane_x();
int w = pane_w();
int fh = system_font_height();
int y = TAB_H + PAD;
if (g_decision_count == 0) {
c.text(x, y, "No consent decisions are stored.", theme.text_subtle);
c.text(x, y + fh + 8, "Programs without a grant ask the first time they need", theme.text_muted);
c.text(x, y + (fh + 8) * 2, "storage, network, time, display or log access.", theme.text_muted);
return;
}
const montauk::consent::Decision& d = g_decisions[g_selected_decision];
c.text(x, y, base_name(d.path), theme.text);
const char* state = d.allow ? "Allowed" : "Denied";
c.text(x + w - text_width(state), y, state, d.allow ? theme.accent : theme.danger);
y += fh + 6;
draw_text_fit(c, x, y, d.path, w, theme.text_subtle);
y += fh + 14;
mtk::draw_separator(c, x, y, w, theme);
y += 14;
int ci = cap_index(d.capability);
c.text(x, y, "CAPABILITY", theme.text_muted);
y += fh + 6;
c.text(x + 10, y, ci >= 0 ? montauk::caps::NAMES[ci].name : "?", theme.text);
y += fh + 4;
if (ci >= 0) draw_text_fit(c, x + 10, y, kCapDescriptions[ci], w - 10, theme.text_subtle);
y += fh + 16;
c.text(x, y, "Forget makes the program ask again the next time it needs", theme.text_muted);
y += fh + 4;
c.text(x, y, "this. A copy already running keeps its access until it exits.", theme.text_muted);
}
static void draw_locked(Canvas& c, const mtk::Theme& theme) {
int fh = system_font_height();
int y = TAB_H + 60;
@@ -564,9 +645,13 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
int flagged = 0;
for (int i = 0; i < g_grant_count; i++)
if (g_grants[i].issue_count > 0) flagged++;
snprintf(summary, sizeof(summary), "%d grant%s%s, %d flagged - read-only view",
g_grant_count, g_grant_count == 1 ? "" : "s",
g_truncated ? " (list truncated)" : "", flagged);
if (g_tab == TAB_CONSENT)
snprintf(summary, sizeof(summary), "%d stored decision%s",
g_decision_count, g_decision_count == 1 ? "" : "s");
else
snprintf(summary, sizeof(summary), "%d grant%s%s, %d flagged - read-only view",
g_grant_count, g_grant_count == 1 ? "" : "s",
g_truncated ? " (list truncated)" : "", flagged);
}
c.text(PAD, foot.y + (foot.h - system_font_height()) / 2, summary, theme.text_subtle);
@@ -575,6 +660,11 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
mtk::draw_button(c, reload, "Reload", mtk::BUTTON_SECONDARY,
mtk::widget_state(false, mouse_in(reload), true), theme);
}
if (forget_visible()) {
Rect forget = forget_button();
mtk::draw_button(c, forget, "Forget", mtk::BUTTON_DANGER,
mtk::widget_state(false, mouse_in(forget), true), theme);
}
}
static void render() {
@@ -586,6 +676,7 @@ static void render() {
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
draw_list(c, theme);
if (g_tab == TAB_PROGRAMS) draw_programs_pane(c, theme);
else if (g_tab == TAB_CONSENT) draw_consent_pane(c, theme);
else draw_caps_pane(c, theme);
} else {
draw_locked(c, theme);
@@ -598,8 +689,25 @@ static void render() {
static void reload() {
load_grants();
load_decisions();
clamp_list_scroll();
set_status("Reloaded 0:/config/capabilities.toml");
set_status(g_tab == TAB_CONSENT ? "Reloaded 0:/config/consent.toml"
: "Reloaded 0:/config/capabilities.toml");
}
static void forget_selected() {
if (g_decision_count == 0) return;
const montauk::consent::Decision& d = g_decisions[g_selected_decision];
if (montauk::consent::forget(d.id) < 0) {
set_status("Could not write 0:/config/consent.toml");
return;
}
char msg[128];
snprintf(msg, sizeof(msg), "Forgot %s for %s", montauk::consent::name_for(d.capability),
base_name(d.path));
load_decisions();
clamp_list_scroll();
set_status(msg);
}
static void select_index(int index) {
@@ -607,8 +715,7 @@ static void select_index(int index) {
if (count <= 0) return;
if (index < 0) index = 0;
if (index >= count) index = count - 1;
if (g_tab == TAB_PROGRAMS) g_selected_grant = index;
else g_selected_cap = index;
*selected_for_tab() = index;
int visible = list_visible_rows();
if (index < g_list_scroll) g_list_scroll = index;
@@ -619,7 +726,7 @@ static void select_index(int index) {
static void switch_tab(Tab tab) {
g_tab = tab;
g_list_scroll = 0;
select_index(tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
select_index(*selected_for_tab());
}
static void handle_mouse(const montauk::abi::WinEvent& ev) {
@@ -675,6 +782,7 @@ static void handle_mouse(const montauk::abi::WinEvent& ev) {
}
}
if (reload_button().contains(mx, my)) reload();
if (forget_visible() && forget_button().contains(mx, my)) forget_selected();
}
static bool handle_key(const montauk::abi::KeyEvent& key) {
@@ -685,10 +793,10 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
}
if (!g_is_admin) return false;
if (key.ascii == '\t') {
switch_tab(g_tab == TAB_PROGRAMS ? TAB_CAPS : TAB_PROGRAMS);
switch_tab((Tab)((g_tab + 1) % TAB_COUNT));
return true;
}
int current = g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap;
int current = *selected_for_tab();
if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up
if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down
if (key.ascii == 'r' || key.ascii == 'R') {
@@ -703,7 +811,10 @@ extern "C" void _start() {
g_accent = mtk::load_system_accent();
g_is_admin = session_is_admin();
if (g_is_admin) load_grants();
if (g_is_admin) {
load_grants();
load_decisions();
}
if (!g_win.create("Capabilities", WIN_W, WIN_H)) montauk::exit(1);
render();
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+449
View File
@@ -0,0 +1,449 @@
/*
* consent.cpp
* Consent agent: answers the kernel's first-use capability requests from
* stored decisions, or asks the user in a prompt window (docs/consent.md)
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <gui/mtk.hpp>
#include <montauk/consent.h>
// The prompt is an ordinary window inside the desktop, but an INTERNAL one:
// its pixels live in the desktop's heap, not in a window-server surface, so
// no app can map it (SYS_WINMAP) and only real input routed by the desktop
// reaches it. What remains is clickjacking -- an app raising a window over
// the prompt, or stealing focus, just as the user clicks. So Allow only
// arms after the prompt has been focused and on top for ARM_DELAY_MS without
// interruption; losing either restarts the delay. Don't Allow and closing
// are always safe and never wait.
namespace {
constexpr int PROMPT_W = 460;
constexpr int PAD = 20;
constexpr int ICON = 32;
constexpr int BTN_W = 120;
constexpr int BTN_GAP = 12;
constexpr int MAX_LINES = 6;
constexpr int LINE_CHARS = 160;
constexpr uint64_t ARM_DELAY_MS = 700;
struct ConsentPrompt {
DesktopState* ds;
montauk::abi::ConsentRequest req;
char headline[MAX_LINES][LINE_CHARS];
int headline_lines;
char path[2][LINE_CHARS];
int path_lines;
uint64_t opened_at;
uint64_t armed_since; // 0 while not both focused and on top
bool armed;
bool answered;
bool hover_allow;
bool hover_deny;
};
bool g_registered = false;
ConsentPrompt* g_open = nullptr; // one prompt at a time; the rest wait in the kernel
Color g_icon_fill = {};
// Prompt icons, loaded on first use: one per capability plus the fallback.
struct CachedIcon {
const char* path;
SvgIcon icon;
};
CachedIcon g_icons[8] = {};
const SvgIcon* icon_at(const char* path) {
for (auto& c : g_icons) {
if (c.path && montauk::streq(c.path, path)) return c.icon.pixels ? &c.icon : nullptr;
if (c.path == nullptr) {
c.path = path;
c.icon = svg_load(path, ICON, ICON, g_icon_fill);
return c.icon.pixels ? &c.icon : nullptr;
}
}
return nullptr;
}
const SvgIcon* icon_for(uint64_t capability) {
const SvgIcon* icon = icon_at(montauk::consent::icon_for(capability));
return icon ? icon : icon_at(montauk::consent::DEFAULT_ICON);
}
// ==== Text ====
// Greedy word wrap into fixed line buffers. A word wider than the line is
// hard-broken so hostile input cannot overflow the layout.
int wrap_text(const char* text, int max_w, char (*lines)[LINE_CHARS], int max_lines) {
int count = 0;
const char* p = text;
while (*p && count < max_lines) {
while (*p == ' ') p++;
if (!*p) break;
char* line = lines[count];
int len = 0;
int last_break = -1;
const char* resume = p;
while (*p && len < LINE_CHARS - 1) {
line[len] = *p;
line[len + 1] = '\0';
if (text_width(line) > max_w && len > 0) {
line[len] = '\0';
break;
}
if (*p == ' ') {
last_break = len;
resume = p + 1;
}
len++;
p++;
}
line[len] = '\0';
if (*p && *p != ' ' && last_break > 0) {
// Back up to the last space so a word is not split.
line[last_break] = '\0';
p = resume;
}
count++;
}
if (*p && count == max_lines) {
// Out of lines: mark the cut rather than hide it.
char* last = lines[max_lines - 1];
int len = montauk::slen(last);
if (len > LINE_CHARS - 4) len = LINE_CHARS - 4;
while (len > 0 && text_width(last) + text_width("...") > max_w) last[--len] = '\0';
last[len] = '.'; last[len + 1] = '.'; last[len + 2] = '.'; last[len + 3] = '\0';
}
return count;
}
// The file name is chosen by whoever put the binary there. Show it as
// printable ASCII so control bytes cannot fake the prompt's layout.
void make_printable(char* s) {
for (; *s; s++) {
if (*s < 0x20 || *s >= 0x7F) *s = '?';
}
}
// "0:/apps/foo/foo.elf" -> "foo" when no installed app claims the path.
void app_name_for(const char* path, char* out, int cap) {
for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i];
if (row.external && montauk::streq(row.binary_path, path)) {
montauk::strncpy(out, row.label, cap);
return;
}
}
const char* base = path;
for (const char* p = path; *p; p++) {
if (*p == '/' || *p == ':') base = p + 1;
}
montauk::strncpy(out, base, cap);
int len = montauk::slen(out);
if (len > 4 && montauk::streq(out + len - 4, ".elf")) out[len - 4] = '\0';
}
// ==== Layout ====
gui::mtk::Theme prompt_theme(const DesktopState* ds) {
return gui::mtk::make_theme(ds->settings.accent_color);
}
int text_w() {
return PROMPT_W - 2 * BORDER_WIDTH - PAD * 2 - ICON - 14;
}
int line_h() {
return system_font_height() + 5;
}
Rect allow_rect(int cw, int ch, const gui::mtk::Theme& theme) {
return {cw - PAD - BTN_W * 2 - BTN_GAP, ch - PAD - theme.control_h, BTN_W, theme.control_h};
}
Rect deny_rect(int cw, int ch, const gui::mtk::Theme& theme) {
return {cw - PAD - BTN_W, ch - PAD - theme.control_h, BTN_W, theme.control_h};
}
int content_height(const ConsentPrompt* cp, const gui::mtk::Theme& theme) {
int h = PAD;
h += cp->headline_lines * line_h() + 10;
h += cp->path_lines * line_h() + 12;
h += line_h() * 2 + 8; // trust advice
if (!cp->req.imageProtected) h += line_h() * 2 + 8;
h += theme.control_h + PAD;
return h;
}
// ==== Answering ====
ConsentPrompt* prompt_of(Window* win) {
return win ? (ConsentPrompt*)win->app_data : nullptr;
}
// `remember` is the user's intent; it only takes effect for an installed
// program, since any other binary can be swapped at the same path.
void answer(ConsentPrompt* cp, bool allow, bool remember) {
if (cp->answered) return;
cp->answered = true;
if (remember && cp->req.imageProtected &&
montauk::consent::record(cp->req.imagePath, cp->req.capability, allow) < 0) {
montauk::print("[desktop] consent: could not save the decision to 0:/config/consent.toml\n");
}
montauk::consent_reply(cp->req.id, allow ? montauk::abi::CONSENT_ALLOW
: montauk::abi::CONSENT_DENY);
}
void close_prompt(ConsentPrompt* cp) {
DesktopState* ds = cp->ds;
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].app_data == cp) {
desktop_close_window(ds, i);
return;
}
}
}
bool prompt_is_front(const DesktopState* ds, const Window* win) {
return !ds->screen_locked && ds->window_count > 0 &&
win == &ds->windows[ds->window_count - 1] && win->focused &&
win->state != WIN_MINIMIZED && win->state != WIN_CLOSED;
}
// ==== Window callbacks ====
void prompt_on_draw(Window* win, Framebuffer& fb) {
(void)fb;
ConsentPrompt* cp = prompt_of(win);
if (!cp) return;
gui::mtk::Theme theme = prompt_theme(cp->ds);
Canvas c(win);
c.fill(theme.window_bg);
if (const SvgIcon* icon = icon_for(cp->req.capability)) c.icon(PAD, PAD, *icon);
int x = PAD + ICON + 14;
int y = PAD;
int lh = line_h();
for (int i = 0; i < cp->headline_lines; i++, y += lh)
c.text(x, y, cp->headline[i], theme.text);
y += 10;
for (int i = 0; i < cp->path_lines; i++, y += lh)
c.text(x, y, cp->path[i], theme.text_subtle);
y += 12;
c.text(x, y, "Only allow this if you trust the app and", theme.text_muted);
c.text(x, y + lh, "expected it to ask.", theme.text_muted);
if (!cp->req.imageProtected) {
y += lh * 2 + 8;
c.text(x, y, "This app is not installed in 0:/apps, so the", theme.text_muted);
c.text(x, y + lh, "answer applies only until it exits.", theme.text_muted);
}
gui::mtk::draw_button(c, allow_rect(c.w, c.h, theme), "Allow",
gui::mtk::BUTTON_SECONDARY,
gui::mtk::widget_state(false, cp->hover_allow && cp->armed, cp->armed),
theme);
gui::mtk::draw_button(c, deny_rect(c.w, c.h, theme), "Don't Allow",
gui::mtk::BUTTON_PRIMARY,
gui::mtk::widget_state(false, cp->hover_deny, true),
theme);
}
void prompt_on_poll(Window* win) {
ConsentPrompt* cp = prompt_of(win);
if (!cp || cp->answered) return;
uint64_t now = montauk::get_milliseconds();
// The kernel has already denied it; an answer now would be refused.
if (now - cp->opened_at >= montauk::abi::CONSENT_TIMEOUT_MS) {
cp->answered = true;
close_prompt(cp);
return;
}
bool front = prompt_is_front(cp->ds, win);
if (!front) {
cp->armed_since = 0;
} else if (cp->armed_since == 0) {
cp->armed_since = now;
}
bool armed = front && now - cp->armed_since >= ARM_DELAY_MS;
if (armed != cp->armed) {
cp->armed = armed;
win->dirty = true;
}
}
void prompt_on_mouse(Window* win, MouseEvent& ev) {
ConsentPrompt* cp = prompt_of(win);
if (!cp || cp->answered) return;
gui::mtk::Theme theme = prompt_theme(cp->ds);
gui::mtk::DesktopHost host(win);
int mx = 0;
int my = 0;
if (!host.map_mouse(ev, &mx, &my)) return;
cp->hover_allow = allow_rect(win->content_w, win->content_h, theme).contains(mx, my);
cp->hover_deny = deny_rect(win->content_w, win->content_h, theme).contains(mx, my);
if (!ev.left_pressed()) return;
if (cp->hover_deny) {
answer(cp, false, true);
close_prompt(cp);
} else if (cp->hover_allow && cp->armed && prompt_is_front(cp->ds, win)) {
answer(cp, true, true);
close_prompt(cp);
}
}
void prompt_on_key(Window* win, const montauk::abi::KeyEvent& key) {
ConsentPrompt* cp = prompt_of(win);
if (!cp || cp->answered || !key.pressed) return;
if (key.scancode == 0x01) { // Escape: dismiss, ask again later
answer(cp, false, false);
close_prompt(cp);
} else if ((key.ascii == '\n' || key.ascii == '\r') && cp->armed) {
// Return picks the default, Don't Allow. Armed so a key held while
// the prompt appeared does not answer it.
answer(cp, false, true);
close_prompt(cp);
}
}
void prompt_on_close(Window* win) {
ConsentPrompt* cp = prompt_of(win);
if (!cp) return;
// Closed without an answer (title-bar close, session ending): deny this
// time only.
answer(cp, false, false);
if (g_open == cp) g_open = nullptr;
montauk::mfree(cp);
win->app_data = nullptr;
}
// ==== Opening ====
// Centre over the requesting app's window when it has one, so the prompt is
// visibly about that app; otherwise centre on the screen.
void place_prompt(DesktopState* ds, int pid, int w, int h, int* out_x, int* out_y) {
int cx = ds->screen_w / 2;
int cy = ds->screen_h / 2;
montauk::abi::WinInfo infos[MAX_WINDOWS];
int count = montauk::win_enumerate(infos, MAX_WINDOWS);
for (int e = 0; e < count; e++) {
if (infos[e].ownerPid != pid) continue;
for (int i = 0; i < ds->window_count; i++) {
const Window& win = ds->windows[i];
if (!win.external || win.ext_win_id != infos[e].id) continue;
if (win.state == WIN_MINIMIZED || win.state == WIN_CLOSED) continue;
cx = win.frame.x + win.frame.w / 2;
cy = win.frame.y + win.frame.h / 2;
e = count;
break;
}
}
int x = cx - w / 2;
int y = cy - h / 2;
if (x + w > ds->screen_w) x = ds->screen_w - w;
if (y + h > ds->screen_h) y = ds->screen_h - h;
if (x < 0) x = 0;
if (y < PANEL_HEIGHT) y = PANEL_HEIGHT;
*out_x = x;
*out_y = y;
}
bool open_prompt(DesktopState* ds, const montauk::abi::ConsentRequest& req) {
ConsentPrompt* cp = (ConsentPrompt*)montauk::malloc(sizeof(ConsentPrompt));
if (!cp) return false;
montauk::memset(cp, 0, sizeof(*cp));
cp->ds = ds;
cp->req = req;
cp->opened_at = montauk::get_milliseconds();
char name[48];
app_name_for(req.imagePath, name, sizeof(name));
make_printable(name);
char shown_path[sizeof(req.imagePath)];
montauk::strncpy(shown_path, req.imagePath, sizeof(shown_path));
make_printable(shown_path);
char headline[320];
snprintf(headline, sizeof(headline), "\"%s\" wants to %s.", name,
montauk::consent::action_for(req.capability));
cp->headline_lines = wrap_text(headline, text_w(), cp->headline, MAX_LINES);
// The path is where the user can tell a system app from a download.
cp->path_lines = wrap_text(shown_path, text_w(), cp->path, 2);
gui::mtk::Theme theme = prompt_theme(ds);
int w = PROMPT_W;
int h = content_height(cp, theme) + TITLEBAR_HEIGHT + BORDER_WIDTH;
int x = 0;
int y = 0;
place_prompt(ds, req.pid, w, h, &x, &y);
int idx = desktop_create_window(ds, "Permission Request", x, y, w, h);
if (idx < 0) {
montauk::mfree(cp);
return false;
}
Window* win = &ds->windows[idx];
win->app_data = cp;
win->on_draw = prompt_on_draw;
win->on_mouse = prompt_on_mouse;
win->on_key = prompt_on_key;
win->on_poll = prompt_on_poll;
win->on_close = prompt_on_close;
g_open = cp;
return true;
}
} // namespace
// ==== Agent ====
void desktop_consent_init(DesktopState* ds) {
// Every session registers; a standard session simply never receives a
// request, because it has none of the eligible capabilities to delegate.
g_registered = montauk::consent_register() == 0;
if (!g_registered) {
montauk::print("[desktop] consent: could not register as the consent agent\n");
return;
}
g_icon_fill = gui::mtk::make_theme(ds->settings.accent_color).text;
}
bool desktop_consent_poll(DesktopState* ds) {
if (!g_registered || g_open != nullptr) return false;
// Drain whatever the stored decisions settle; stop at the first request
// that needs the user.
for (;;) {
montauk::abi::ConsentRequest req;
if (montauk::consent_next(&req) != 1) return false;
int stored = req.imageProtected
? montauk::consent::lookup(req.imagePath, req.capability) : -1;
if (stored >= 0) {
montauk::consent_reply(req.id, stored ? montauk::abi::CONSENT_ALLOW
: montauk::abi::CONSENT_DENY);
continue;
}
if (!open_prompt(ds, req)) {
// Could not show it, so the user cannot have agreed.
montauk::consent_reply(req.id, montauk::abi::CONSENT_DENY);
continue;
}
return true;
}
}
@@ -181,6 +181,10 @@ inline gui::Rect desktop_content_rect(const gui::Window* win) {
gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my);
gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
// consent.cpp
void desktop_consent_init(gui::DesktopState* ds);
bool desktop_consent_poll(gui::DesktopState* ds);
// panel.cpp
void desktop_draw_app_menu(gui::DesktopState* ds);
void desktop_draw_net_popup(gui::DesktopState* ds);
+4
View File
@@ -393,6 +393,7 @@ void gui::desktop_init(DesktopState* ds) {
desktop_refresh_netifs(ds);
desktop_wifi_init(ds);
desktop_battery_init(ds);
desktop_consent_init(ds);
ds->vol_popup_open = false;
ds->vol_icon_rect = {0, 0, 0, 0};
@@ -796,6 +797,9 @@ void gui::desktop_run(DesktopState* ds) {
// Poll external windows (discover new, remove dead, update dirty)
sceneChanged |= desktop_poll_external_windows(ds);
// Answer or prompt for pending first-use capability requests.
if (!ds->screen_locked) sceneChanged |= desktop_consent_poll(ds);
if (!ds->screen_locked) {
// Poll windows that have a poll callback
for (int i = 0; i < ds->window_count; i++) {
+2 -4
View File
@@ -59,11 +59,10 @@ void launch_session(LoginState* ls) {
// session is delegable for the same reason an admin one is -- otherwise
// its own terminal could not hand the shell the suspend it already has.
montauk::abi::SpawnCapabilities caps{};
caps.permitted = montauk::user::is_admin(ls->username)
caps.effective = montauk::user::is_admin(ls->username)
? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION;
caps.effective = caps.permitted;
caps.delegable = caps.permitted;
caps.delegable = caps.effective;
int pid;
if (ls->session_mode == SESSION_CONSOLE) {
@@ -74,7 +73,6 @@ void launch_session(LoginState* ls) {
} else {
// The desktop is also the compositor. That authority is not the
// session's to pass on, so it is added here and not delegable.
caps.permitted |= montauk::abi::CAP_COMPOSITOR;
caps.effective |= montauk::abi::CAP_COMPOSITOR;
pid = montauk::spawn_with_caps("0:/os/desktop.elf", ls->username,
ls->username, caps);
+2 -3
View File
@@ -27,14 +27,13 @@ static void maybe_run_setup_session() {
montauk::abi::SpawnCapabilities caps{};
bool admin = montauk::streq(role, "admin");
caps.permitted = caps.effective = admin
caps.effective = admin
? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION;
// Matches launch_session(): a session delegates from what it holds, and
// the grant table decides which program receives which part of it.
caps.delegable = caps.permitted;
caps.delegable = caps.effective;
// Compositor authority for the desktop itself, never delegable onwards.
caps.permitted |= montauk::abi::CAP_COMPOSITOR;
caps.effective |= montauk::abi::CAP_COMPOSITOR;
int pid = montauk::spawn_with_caps("0:/os/desktop.elf", user, user, caps);
if (pid >= 0) {