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

+16 -1
View File
@@ -4,6 +4,17 @@
# Capability names: process_admin, power_request, power_control, suspend,
# storage_admin, raw_storage, network_admin, set_time, user_admin,
# display_admin, device_admin, log_read, system_image, compositor and "all".
#
# `effective` is what the program itself may use; `delegable` is what it may
# pass on to programs it launches. The two are independent, and both are
# clamped by the kernel to what the launcher can delegate.
#
# A grant only applies to a program under 0:/os or 0:/apps. Anything else can
# be replaced by any process, so the kernel starts it with no capabilities.
#
# A program with no grant here can still ask for storage_admin, network_admin,
# set_time, display_admin or log_read the first time it needs one: the desktop
# prompts, and the answer is stored in 0:/config/consent.toml (docs/consent.md).
# ==== System services (started by init) ====
@@ -17,7 +28,6 @@ effective = ["set_time"]
[grant.login]
path = "0:/os/login.elf"
permitted = ["all", "compositor"]
effective = [
"power_control", "storage_admin", "device_admin",
"user_admin", "process_admin", "log_read",
@@ -86,6 +96,11 @@ effective = ["set_time"]
path = "0:/apps/bluetooth/bluetooth.elf"
effective = ["device_admin"]
# Writes 0:/config/consent.toml when a stored consent decision is forgotten.
[grant.capabilities]
path = "0:/apps/capabilities/capabilities.elf"
effective = ["user_admin"]
[grant.syslog]
path = "0:/apps/syslog/syslog.elf"
effective = ["log_read"]
+44 -9
View File
@@ -265,6 +265,13 @@ namespace montauk::abi {
/* Storage.hpp */
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
/* Consent.hpp -- first-use consent prompts (docs/consent.md). Only the
session's consent agent (the desktop) may register, drain requests
and answer them. */
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
/* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -309,26 +316,55 @@ namespace montauk::abi {
"login grants it to the desktop directly");
static constexpr int SYS_ERR_PERMISSION = -13;
// A process holds two independent sets: what it may use (effective) and
// what it may pass to children (delegable). A launcher can delegate what
// it never uses itself, so neither set bounds the other.
struct SpawnCapabilities {
uint64_t permitted;
uint64_t effective;
uint64_t delegable;
};
// Both of the child's sets must come out of the parent's delegable set.
constexpr bool ValidCapabilityDelegation(const SpawnCapabilities& child,
uint64_t parentDelegable) {
return (child.permitted & ~CAP_ALL) == 0 &&
(child.effective & ~child.permitted) == 0 &&
(child.delegable & ~child.permitted) == 0 &&
(child.permitted & ~parentDelegable) == 0 &&
return ((child.effective | child.delegable) & ~CAP_ALL) == 0 &&
(child.effective & ~parentDelegable) == 0 &&
(child.delegable & ~parentDelegable) == 0;
}
static_assert(ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
{CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
static_assert(ValidCapabilityDelegation(
{0, CAP_NETWORK_ADMIN}, CAP_NETWORK_ADMIN));
static_assert(!ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
static_assert(!ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_ALL));
{CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_NETWORK_ADMIN));
static_assert(!ValidCapabilityDelegation(
{0, CAP_NETWORK_ADMIN | CAP_SET_TIME}, CAP_NETWORK_ADMIN));
/* Capabilities a user can grant at first use through a consent prompt.
A consent grant never exceeds what the process's parent could have
delegated to it at spawn (its consent ceiling), is never delegable
onwards, and is issued only by the session's consent agent -- so it is
deferred delegation, not amplification. */
static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
CAP_DISPLAY_ADMIN | CAP_LOG_READ;
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
"consent can only hand out authority an admin session holds");
static constexpr int CONSENT_DENY = 0;
static constexpr int CONSENT_ALLOW = 1;
// A request left unanswered this long is denied (and not remembered).
static constexpr uint64_t CONSENT_TIMEOUT_MS = 120000;
struct ConsentRequest {
uint64_t id;
uint64_t capability; // exactly one CAP_* bit
int32_t pid;
uint32_t imageProtected; // 1 when writing the image needs CAP_SYSTEM_IMAGE
char imagePath[256];
};
static constexpr int USB_ERR_INVALID = -1;
@@ -837,7 +873,6 @@ namespace montauk::abi {
char name[64];
uint64_t heapUsed; // Distance from UserHeapBase to high-water mark
uint64_t cpuTimeMs; // accumulated scheduler runtime
uint64_t permittedCaps;
uint64_t effectiveCaps;
uint64_t delegableCaps;
};
+3
View File
@@ -214,6 +214,9 @@ extern "C" {
#define MTK_SYS_BATTERYINFO 188
#define MTK_SYS_GETCAPS 189
#define MTK_SYS_VOLINFO 190
#define MTK_SYS_CONSENT_REGISTER 191
#define MTK_SYS_CONSENT_NEXT 192
#define MTK_SYS_CONSENT_REPLY 193
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
+31 -14
View File
@@ -56,6 +56,32 @@ namespace caps {
return 0;
}
// True when a grant for `path` can apply: the program is installed under
// 0:/os or 0:/apps, which cannot be rewritten without system_image. The
// kernel enforces this (Fs::IsSystemImage) by starting any other program
// without capabilities; this copy of the rule is only for display.
inline bool is_installed_path(const char* path) {
if (path == nullptr) return false;
const char* p = path;
uint64_t drive = 0;
if (*p < '0' || *p > '9') return false;
while (*p >= '0' && *p <= '9') drive = drive * 10 + (uint64_t)(*p++ - '0');
if (*p != ':' || drive != 0) return false;
p++;
static const char* const roots[] = {"/os", "/apps"};
for (const char* root : roots) {
int i = 0;
while (root[i]) {
char c = p[i];
if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
if (c != root[i]) break;
i++;
}
if (root[i] == '\0' && (p[i] == '\0' || p[i] == '/')) return true;
}
return false;
}
// Read an array-of-strings key into a capability mask.
inline uint64_t mask_from_key(const montauk::toml::Doc& doc, const char* key) {
montauk::toml::Value* arr = doc.get_array(key);
@@ -97,7 +123,7 @@ namespace caps {
// path has no entry, which is the common case and means "no authority".
inline bool lookup(const char* binary_path,
montauk::abi::SpawnCapabilities& out) {
out = {0, 0, 0};
out = {0, 0};
if (binary_path == nullptr || binary_path[0] == '\0') return false;
montauk::toml::Doc doc = montauk::config::load(GRANT_CONFIG);
@@ -117,15 +143,7 @@ namespace caps {
uint64_t effective = mask_from_key(doc, sibling);
build_sibling_key(entry->key, "delegable", sibling, sizeof(sibling));
uint64_t delegable = mask_from_key(doc, sibling);
build_sibling_key(entry->key, "permitted", sibling, sizeof(sibling));
uint64_t permitted = mask_from_key(doc, sibling);
// A grant that does not name `permitted` owns exactly what it can
// use or pass on. Declaring it separately is only needed by a
// supervisor that holds authority in reserve (login).
if (permitted == 0) permitted = effective | delegable;
out.permitted = permitted;
out.effective = effective;
out.delegable = delegable;
found = true;
@@ -137,7 +155,7 @@ namespace caps {
// The calling process's own capability masks.
inline bool self(montauk::abi::SpawnCapabilities& out) {
out = {0, 0, 0};
out = {0, 0};
return montauk::getcaps(&out) == 0;
}
@@ -150,15 +168,14 @@ namespace caps {
// actually delegate.
inline montauk::abi::SpawnCapabilities for_binary(const char* binary_path,
uint64_t caller_delegable) {
montauk::abi::SpawnCapabilities caps{0, 0, 0};
montauk::abi::SpawnCapabilities caps{0, 0};
if (caller_delegable == 0) return caps;
if (!lookup(binary_path, caps)) return caps;
caps.permitted &= caller_delegable;
caps.effective &= caps.permitted;
caps.delegable &= caps.permitted;
caps.effective &= caller_delegable;
caps.delegable &= caller_delegable;
return caps;
}
+209
View File
@@ -0,0 +1,209 @@
/*
* consent.h
* First-use consent: prompt wording and the stored-decision table
* (0:/config/consent.toml). See docs/consent.md.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/toml.h>
#include <montauk/config.h>
#include <montauk/capabilities.h>
namespace montauk {
namespace consent {
inline constexpr const char* STORE_CONFIG = "consent";
inline constexpr const char* STORE_PREFIX = "decision.";
struct CapText {
uint64_t bit;
const char* action; // completes "<App> wants to ..."
const char* icon; // shown in the prompt
};
inline constexpr CapText TEXTS[] = {
{montauk::abi::CAP_STORAGE_ADMIN, "format, partition and mount disks",
"0:/os/icons/drive-harddisk.svg"},
{montauk::abi::CAP_NETWORK_ADMIN, "change network settings and join Wi-Fi networks",
"0:/os/icons/network-wired.svg"},
{montauk::abi::CAP_SET_TIME, "change the system clock and time zone",
"0:/os/icons/preferences-system-time.svg"},
{montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution and brightness",
"0:/os/icons/preferences-desktop-display.svg"},
{montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing",
"0:/os/icons/utilities-log-viewer.svg"},
};
// Fallback for a capability without its own icon.
inline constexpr const char* DEFAULT_ICON = "0:/os/icons/lock.svg";
inline const char* action_for(uint64_t bit) {
for (const auto& t : TEXTS) {
if (t.bit == bit) return t.action;
}
return "use a protected system capability";
}
inline const char* icon_for(uint64_t bit) {
for (const auto& t : TEXTS) {
if (t.bit == bit) return t.icon;
}
return DEFAULT_ICON;
}
inline const char* name_for(uint64_t bit) {
for (const auto& n : montauk::caps::NAMES) {
if (n.bit == bit) return n.name;
}
return "";
}
// One stored decision, as read from the table. Only programs installed
// under 0:/os or 0:/apps are ever stored: those paths cannot be rewritten
// without CAP_SYSTEM_IMAGE, so the path alone identifies the program.
// The kernel reports this as ConsentRequest::imageProtected.
struct Decision {
char id[16];
char path[256];
uint64_t capability;
bool allow;
};
namespace detail {
inline void key(char* out, int cap, const char* id, const char* field) {
int n = 0;
for (const char* p = STORE_PREFIX; *p && n < cap - 1; p++) out[n++] = *p;
for (const char* p = id; *p && n < cap - 1; p++) out[n++] = *p;
if (n < cap - 1) out[n++] = '.';
for (const char* p = field; *p && n < cap - 1; p++) out[n++] = *p;
out[n] = '\0';
}
// Ids of "decision.<id>.path" entries, in file order.
inline int collect_ids(const montauk::toml::Doc& doc, char (*ids)[16], int max) {
int count = 0;
int prefixLen = montauk::slen(STORE_PREFIX);
for (int i = 0; i < doc.entries.count && count < max; i++) {
montauk::toml::Value* v = doc.entries.items[i];
if (v == nullptr || v->key == nullptr) continue;
if (!montauk::starts_with(v->key, STORE_PREFIX)) continue;
const char* rest = v->key + prefixLen;
int dot = 0;
while (rest[dot] && rest[dot] != '.') dot++;
if (rest[dot] != '.' || !montauk::streq(rest + dot + 1, "path")) continue;
if (dot <= 0 || dot >= 16) continue;
montauk::memcpy(ids[count], rest, dot);
ids[count][dot] = '\0';
count++;
}
return count;
}
inline bool read(const montauk::toml::Doc& doc, const char* id, Decision& out) {
char k[64];
montauk::memset(&out, 0, sizeof(out));
montauk::strncpy(out.id, id, sizeof(out.id));
key(k, sizeof(k), id, "path");
montauk::strncpy(out.path, doc.get_string(k, ""), sizeof(out.path));
key(k, sizeof(k), id, "capability");
out.capability = montauk::caps::bit_for_name(doc.get_string(k, ""));
key(k, sizeof(k), id, "decision");
out.allow = montauk::streq(doc.get_string(k, ""), "allow");
// "all" or an unknown name is never a valid stored decision.
return out.path[0] &&
(out.capability & montauk::abi::CAP_CONSENT_ELIGIBLE) != 0 &&
(out.capability & (out.capability - 1)) == 0;
}
inline void remove_id(montauk::toml::Doc* doc, const char* id) {
static const char* fields[] = {"path", "capability", "decision"};
char k[64];
for (const char* f : fields) {
key(k, sizeof(k), id, f);
montauk::config::unset(doc, k);
}
}
} // namespace detail
// Read up to `max` stored decisions. Returns the count.
inline int load_all(Decision* out, int max) {
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
char ids[128][16];
int idCount = detail::collect_ids(doc, ids, 128);
int count = 0;
for (int i = 0; i < idCount && count < max; i++) {
if (detail::read(doc, ids[i], out[count])) count++;
}
doc.destroy();
return count;
}
// Stored answer for (path, capability): 1 allow, 0 deny, -1 none.
inline int lookup(const char* path, uint64_t capability) {
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
char ids[128][16];
int idCount = detail::collect_ids(doc, ids, 128);
int result = -1;
for (int i = 0; i < idCount && result < 0; i++) {
Decision d;
if (!detail::read(doc, ids[i], d)) continue;
if (d.capability == capability && montauk::streq(d.path, path)) {
result = d.allow ? 1 : 0;
}
}
doc.destroy();
return result;
}
// Store an answer, replacing any earlier one for the same path and
// capability. Returns 0 on success, negative when the table cannot be
// written.
inline int record(const char* path, uint64_t capability, bool allow) {
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
char ids[128][16];
int idCount = detail::collect_ids(doc, ids, 128);
int nextId = 1;
for (int i = 0; i < idCount; i++) {
Decision d;
bool valid = detail::read(doc, ids[i], d);
if (valid && d.capability == capability && montauk::streq(d.path, path))
detail::remove_id(&doc, ids[i]);
int n = 0;
for (const char* p = ids[i]; *p >= '0' && *p <= '9'; p++) n = n * 10 + (*p - '0');
if (n >= nextId) nextId = n + 1;
}
char id[16];
int len = 0;
char rev[16];
for (int n = nextId; n > 0 && len < 15; n /= 10) rev[len++] = (char)('0' + n % 10);
for (int i = 0; i < len; i++) id[i] = rev[len - 1 - i];
id[len] = '\0';
char k[64];
detail::key(k, sizeof(k), id, "path");
montauk::config::set_string(&doc, k, path);
detail::key(k, sizeof(k), id, "capability");
montauk::config::set_string(&doc, k, name_for(capability));
detail::key(k, sizeof(k), id, "decision");
montauk::config::set_string(&doc, k, allow ? "allow" : "deny");
int rc = montauk::config::save(STORE_CONFIG, &doc);
doc.destroy();
return rc;
}
// Drop one stored decision by id; the program is asked again next time.
inline int forget(const char* id) {
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
detail::remove_id(&doc, id);
int rc = montauk::config::save(STORE_CONFIG, &doc);
doc.destroy();
return rc;
}
} // namespace consent
} // namespace montauk
+11
View File
@@ -131,6 +131,17 @@ namespace montauk {
inline int getcaps(montauk::abi::SpawnCapabilities* out) {
return (int)syscall1(montauk::abi::SYS_GETCAPS, (uint64_t)out);
}
// Consent agent (the desktop only; see docs/consent.md).
inline int consent_register() {
return (int)syscall0(montauk::abi::SYS_CONSENT_REGISTER);
}
inline int consent_next(montauk::abi::ConsentRequest* out) {
return (int)syscall1(montauk::abi::SYS_CONSENT_NEXT, (uint64_t)out);
}
inline int consent_reply(uint64_t id, int decision) {
return (int)syscall2(montauk::abi::SYS_CONSENT_REPLY, id, (uint64_t)decision);
}
inline int chdir(const char* path) {
return (int)syscall1(montauk::abi::SYS_CHDIR, (uint64_t)path);
}
+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) {