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();