Skip to content

Instantly share code, notes, and snippets.

@sloonz
Created January 8, 2026 16:43
Show Gist options
  • Select an option

  • Save sloonz/ef282a1f53366e1ed6f5cb848de015ba to your computer and use it in GitHub Desktop.

Select an option

Save sloonz/ef282a1f53366e1ed6f5cb848de015ba to your computer and use it in GitHub Desktop.
name: base
clearenv: true
dieWithParent: true
unsharePid: true
newSession: true
mounts:
/proc: proc
/dev: dev
/tmp: tmpfs
/etc: {bind: {path: /etc, readOnly: true}}
/usr: {bind: {path: /usr, readOnly: true}}
/bin: {symlink: usr/bin}
/sbin: {symlink: usr/sbin}
/lib: {symlink: usr/lib}
/lib64: {symlink: usr/lib}
"~": tmpfs
"~/.config": dir
"~/.cache": dir
"~/.local/share": dir
"{env[XDG_RUNTIME_DIR]}": {tmpfs: {perms: 700}}
/run/systemd/resolve: {bind: {}}
env:
PATH: true
LANG: true
XDG_RUNTIME_DIR: true
XDG_SESSION_TYPE: true
TERM: true
HOME: true
LOGNAME: true
USER: true
# CVE-2017-5226 prevention
# f = seccomp.SyscallFilter(defaction = seccomp.ALLOW)
# f.add_rule(seccomp.KILL_PROCESS, 'ioctl', seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI))
# f.add_rule(seccomp.KILL_PROCESS, 'ioctl', seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCLINUX))
# f.export_bpf(fd)
seccomp:
- arch: x86_64
data: |-
IAAAAAQAAAAVAAAMPgAAwCAAAAAAAAAANQAAAQAAAEAVAAAJ/////xUAAAYQAAAAIAAAABwAAABU
AAAAAAAAABUAAAMAAAAAIAAAABgAAAAVAAIAHFQAABUAAQASVAAABgAAAAAA/38GAAAAAAAAgAYA
AAAAAAAA
vars:
flatpakInfo: |-
[Application]
name={name}
[Instance]
instance-id=bwrap-{pid}
---
name: shell
newSession: false
env:
LS_COLORS: true
WORDCHARS: true
EDITOR: true
PYTHONIOENCODING: true
PROMPT: true
RPROMPT: true
HISTFILE: true
HISTSIZE: true
SAVEHIST: true
NODE_REPL_HISTORY: true
SQLITE_HISTORY: true
PYTHON_HISTORY: true
MYSQL_HISTFILE: true
mounts:
"~/.zshrc": {bind: {readOnly: true}}
"~/.config/git": {bind: {readOnly: true}}
"~/.config/nvim": {bind: {readOnly: true}}
---
name: display
env:
XKB_DEFAULT_LAYOUT: custom
mounts:
"~/.cache/mesa_shader_cache": {bind: {create: true}}
"~/.cache/fontconfig": {bind: {create: true}}
"~/.cache/radv_builtin_shaders": {bind: {create: true}}
"~/.xkb": {bind: {readOnly: true}}
---
name: x11
include: [display]
env:
DISPLAY: true
mounts:
/tmp/.X11-unix/X0: {bind: {readOnly: true}}
---
name: wayland
include: [display]
env:
WAYLAND_DISPLAY: true
mounts:
"{env[XDG_RUNTIME_DIR]}/{env[WAYLAND_DISPLAY]}": {bind: {readOnly: true}}
---
name: audio
mounts:
"{env[XDG_RUNTIME_DIR]}/pulse/native": {bind: {readOnly: true}}
"~/.config/pulse/cookie": {bind: {readOnly: true, try: true}}
"{env[XDG_RUNTIME_DIR]}/pipewire-0": {bind: {readOnly: true}}
---
name: drm
mounts:
/dev/dri: {bind: {dev: true}}
/sys: {bind: {readOnly: true}}
---
name: portal
mounts:
"{env[XDG_RUNTIME_DIR]}/doc": {bind: {path: "{env[XDG_RUNTIME_DIR]}/doc/by-app/{name}", create: true}}
/.flatpak-info: {data: {content: "{flatpakInfo}", readOnly: true}}
"{env[XDG_RUNTIME_DIR]}/flatpak-info": {data: {content: "{flatpakInfo}", readOnly: true}}
dbus:
sandbox:
include: [base]
asPid1: true
mounts:
/.flatpak-info: {data: {content: "{flatpakInfo}", readOnly: true}}
"{env[XDG_RUNTIME_DIR]}/flatpak-info": {data: {content: "{flatpakInfo}", readOnly: true}}
user:
org.kde.*: own
org.freedesktop.portal.*: talk
org.freedesktop.Notifications: talk
---
name: project
include: [base, x11, wayland, rust, shell]
chdir: "{env[HOME]}/workspace"
env:
VSCODE_EXTENSIONS: true
vars:
name: "{project}"
projectRoot: "/opt/data/projects/{project}"
mounts:
"~": {bind: {path: "{projectRoot}/home", create: true}}
"~/workspace": {bind: {path: "{projectRoot}/workspace", create: true}}
"~/.cache": {bind: {path: "{projectRoot}/cache", create: true}}
"{env[VSCODE_EXTENSIONS]}": {bind: {readOnly: true}}
---
name: private-home
include: [base]
mounts:
"~": {bind: {path: "~/.local/share/sandboxes/{name}", create: true}}
"~/.config": {bind: {path: "~/.config/sandboxes/{name}", create: true}}
"~/.cache": {bind: {path: "~/.cache/sandboxes/{name}", create: true}}
---
name: cli
include: [private-home, shell]
matches: [node, npx, npm, yarn, go, tsc, cargo, rustup]
vars:
name: cli
chdir: "{cwd}"
mounts:
"{cwd}": {bind: {}}
---
name: none
matches: [discor, gedit, foot, bnet, swtor, firefox, dolphin]
disableSandbox: true
---
name: default
include: [private-home, x11, wayland, audio]
#!/usr/bin/python
import argparse
import base64
import json
import logging
import os
import pathlib
import platform
import pprint
import re
import shlex
import sys
import yaml
sandboxes_cache = {}
global_sandboxes = {}
bwrap_flags = {"levelPrefix", "unshareAll", "shareNet", "unshareUser", "unshareUserTry", "unshareIpc", "unsharePid", "unshareNet", "unshareUts", "unshareCgroup", "unshareCgroupTry", "disableUserns", "assertUsernsDisabled", "clearenv", "newSession", "dieWithParent", "asPid1"}
bwrap_options = {"argv0", "userns", "userns2", "pidns", "uid", "gid", "hostname", "chdir", "execLabel", "fileLabel", "seccomp", "syncFd", "blockFd", "usernsBlockFd", "infoFd", "jsonStatusFd"}
bwrap_list_options = {"addSeccompFd", "capAdd", "capDrop", "lockFile", "remountRo"}
merge_policy = {
"items": {"mounts", "chmod"},
"literal": bwrap_flags.union(bwrap_options).union({"disableSandbox", "vars.*", "env.*", "dbus.sloppyNames.*", "dbus.sandbox.*", "dbus.user.*", "dbus.system.*"}),
"list": bwrap_list_options.union({"extraArgs"}).union({"matches", "dbus.rules.*.*.*"}),
"dict": {"vars", "env", "dbus", "dbus.sloppyNames", "dbus.sandbox", "dbus.user", "dbus.system", "dbus.rules.*", "dbus.rules.*.*"},
"discard": {"name", "include"},
}
def tagged_append(tag, dest):
class TaggedAppend(argparse.Action):
def __call__(self, parser, ns, values, option_strings=None):
dest.append((tag, values))
return TaggedAppend
def load_sandboxes_file(path, default_name=None):
logging.debug("loading %s", path)
with open(path) as fd:
sandboxes = list(yaml.safe_load_all(fd))
sandboxes = [sandboxes] if isinstance(sandboxes, dict) else sandboxes
for i, sb in enumerate(sandboxes):
if name := sb.get("name", default_name if i == 0 else None):
sandboxes_cache[name] = sb
return sandboxes
def try_load_sandbox(name):
if name in sandboxes_cache:
return sandboxes_cache[name]
if name in global_sandboxes:
return global_sandboxes[name]
candidate_paths = [
pathlib.Path(name),
pathlib.Path(f"{name}.yml"),
pathlib.Path(f"{name}.yaml"),
pathlib.Path.home() / ".config" / "sandbox" / f"{name}.yml",
pathlib.Path.home() / ".config" / "sandbox" / f"{name}.yaml",
]
for p in candidate_paths:
if p.exists():
return load_sandboxes_file(p, name)[0]
def load_sandbox(name):
sb = try_load_sandbox(name)
if sb is None:
raise Exception(f"sandbox not found: {name}")
return sb
def get_merge_policy(path, k):
kl = ".".join(path + [k])
kg = ".".join(path + ["*"])
for p, s in merge_policy.items():
if kl in s:
return p, path + [k]
if kg in s:
return p, path + ["*"]
raise Exception(f"Unknown key while merging: {".".join(path + [k])}")
def merge(a, b, path=[]):
res = {}
for k in set(a.keys()).union(b.keys()):
policy, key_path = get_merge_policy(path, k)
if policy == "list":
res[k] = a.get(k, []) + b.get(k, [])
elif policy == "items":
left = a.get(k, {})
right = b.get(k, {})
res[k] = (list(left.items()) if isinstance(left, dict) else left) + \
(list(right.items()) if isinstance(right, dict) else right)
elif policy == "dict":
res[k] = merge(a.get(k, {}), b.get(k, {}), key_path)
elif policy == "literal":
res[k] = b.get(k, a.get(k, None))
return res
def merge_sandboxes(sandboxes):
def load_include(inc):
inc = {"name": inc} if isinstance(inc, str) else inc
if name := inc.get("name"):
if inc.get("try"):
return try_load_sandbox(name)
else:
return load_sandbox(name)
elif path := inc.get("path"):
if inc.get("try") and not os.path.exists(path):
return {}
else:
return load_sandboxes_file(path)[0]
else:
assert False
res = {}
for sb in sandboxes:
inc = merge_sandboxes(load_include(child_sb) for child_sb in sb.get("include", []))
res = merge(res, merge(inc, sb))
return res
def get_sandbox(sb, default_vars):
# Parse vars & env
raw_vars = {**sb.get("vars", {})}
raw_env = {}
vars = {**default_vars}
env = {}
env_unset = set()
for k, v in sb.get("env", {}).items():
if v is True: # inherit
if k in os.environ:
env[k] = os.environ[k]
elif v is False: # clear
env_unset.add(k)
elif v is None: # nothing
pass
elif isinstance(v, str):
raw_env[k] = v
elif isinstance(v, dict):
if "inherits" in v:
if k in os.environ or "defaultValue" not in v:
env[k] = os.environ[k]
else:
raw_env[k] = v["defaultValue"]
elif "value" in v:
if v.get("raw", False):
env[k] = v["value"]
else:
raw_env[k] = v["value"]
else:
raise Exception(f"Invalid value for environment variable {k}: {repr(v)}")
else:
raise Exception(f"Invalid value for environment variable {k}: {repr(v)}")
format_vars = {**vars, "env": {**os.environ, **env}}
while True:
changed = False
for parsed, raw in ((vars, raw_vars), (env, raw_env)):
for k in list(raw.keys()):
try:
parsed[k] = raw[k].format(**format_vars)
del raw[k]
changed = True
except KeyError:
pass
format_vars = {**vars, "env": {**os.environ, **env}}
if not raw_env and not raw_vars:
break
if not changed:
assert False # circular definition
# Parse mounts & chmod
mounts = {os.path.expanduser(k.format(**format_vars).rstrip("/")): v for k, v in sb.get("mounts", []) if v}
chmod = {os.path.expanduser(k.format(**format_vars).rstrip("/")): v for k, v in sb.get("chmod", []) if v}
return {**sb, "vars": vars, "env": env, "envUnset": env_unset, "mounts": mounts, "chmod": chmod}
def pipefd(data):
pr, pw = os.pipe2(0)
if os.fork() == 0:
os.close(pr)
os.write(pw, data)
sys.exit(0)
else:
os.close(pw)
return pr
def get_bwrap_args(sb):
def bwrap_name(name):
if name == "argv0":
return name
return re.sub(r"(?<=[a-z])([A-Z0-9+])", lambda m: "-" + m.group(1).lower(), name)
def format_seccomp_value(value):
if isinstance(value, dict): # {data: string, arch: string}
value = [value]
if isinstance(value, list): # {data: string, arch: string}[]
data = [base64.b64decode(prog["data"]) for prog in value if prog["arch"] == platform.machine()]
if len(data) == 0:
raise Exception(f"seccomp program not found for that architecture: {platform.machine()}")
return str(pipefd(data[0]))
else: # fd
return str(value)
def format_option_value(name, value):
if name in ("seccomp", "addSeccomp"):
return format_seccomp_value(value)
elif name in ("userns", "userns2", "pidns","syncFd", "blockFd", "userNsBlockFd", "infoFd", "jsonStatusFd"):
return str(value)
else:
return str(value).format(**format_vars)
def format_datasource_value(value): # {fd: number} | {content: string, raw?: boolean, base64?: boolean}
if (fd := value.get("fd")) is not None:
return str(fd)
elif (content := value.get("content")) is not None:
if not value.get("raw"):
content = content.format(**format_vars)
if value.get("base64"):
content = base64.b64decode(content)
else:
content = content.encode("utf-8")
return str(pipefd(content))
else:
raise NotImplementedError
format_vars = {**sb["vars"], "env": {**os.environ, **sb["env"]}}
args = [f"--{bwrap_name(f)}" for f in bwrap_flags if sb.get(f)]
for o in bwrap_options:
if sb.get(o) not in (False, None):
args.extend((f"--{bwrap_name(o)}", format_option_value(o, sb[o])))
for o in bwrap_list_options:
for v in sb.get(o, []):
if v not in (False, None):
args.extend((f"--{bwrap_name(o)}", format_option_value(o, v)))
args.extend(arg.format(**format_vars) for arg in sb.get("extraArgs", []))
for e in sb["envUnset"]:
args.extend(("--unsetenv", e))
for k, v in sb["env"].items():
args.extend(("--setenv", k, v))
for dest_path, mount in sorted(sb["mounts"].items()):
if mount in ("proc", "dev", "tmpfs", "mqueue", "dir"):
args.extend((f"--{mount}", dest_path.format(**format_vars)))
elif isinstance(mount, dict):
if (tmpfs := mount.get("tmpfs")) is not None: # { tmpfs: { perms?: number; size?: number }}
if (perms := tmpfs.get("perms")) is not None:
args.extend(("--perms", str(perms)))
if (size := tmpfs.get("size")) is not None:
args.extend(("--size", str(size)))
args.extend(("--tmpfs", dest_path))
elif (dir := mount.get("dir")) is not None: # { dir: { perms?: number }}
if (perms := dir.get("perms")) is not None:
args.extend(("--perms", str(perms)))
args.extend(("--dir", dest_path))
elif (symlink := mount.get("symlink")) is not None: # { symlink: string }
args.extend(("--symlink", symlink.format(**format_vars), dest_path))
elif (bind := mount.get("bind")) is not None: # { bind: { path: string; readOnly?: boolean; dev?: boolean; try?: boolean, create?: boolean }}
prefix = "dev-" if bind.get("dev") else "ro-" if bind.get("ro") else ""
suffix = "-try" if bind.get("try") else ""
src_path = os.path.expanduser(bind.get("path", dest_path).format(**format_vars))
if bind.get("create"):
os.makedirs(src_path, exist_ok=True)
args.extend(("--" + prefix + "bind" + suffix, src_path, dest_path))
elif (fd := mount.get("fd")) is not None: # { fd: { fd: number; readOnly?: boolean }}
args.extend(("--ro-bind-fd" if fd.get("readOnly") else "--bind-fd", str(fd["fd"])))
elif (file := mount.get("file")) is not None: # { file: DataSource & { perms?: number }}
if (perms := file.get("perms")) is not None:
args.extend(("--perms", str(perms)))
args.extend(("--file", format_datasource_value(file), dest_path))
elif (data := mount.get("data")) is not None: # { data: DataSource & { readOnly?: boolean; perms?: number }}
if (perms := data.get("perms")) is not None:
args.extend(("--perms", str(perms)))
args.extend(("--ro-bind-data" if data.get("readOnly") else "--bind-data", format_datasource_value(data), dest_path))
elif (overlay := mount.get("overlay")) is not None: # { overlay: { lower: string[]; upper?: string; work?: string; mode?: "rw" | "tmp" | "ro" }}
for lower in overlay["lower"]:
args.extend(("--overlay-src", lower.format(**format_vars)))
mode = overlay.get("mode", "rw" if "upper" in overlay and "work" in overlay else "tmp")
if mode == "rw":
args.extend(("--overlay", overlay["upper"], overlay["work"], dest_path))
else:
args.extend((f"--{mode}-overlay", dest_path))
else:
raise Exception(f"invalid mount value: {repr(mount)}")
else:
raise Exception(f"invalid mount value: {repr(mount)}")
for path, mode in sorted(sb["chmod"]):
args.extend(("--chmod", path.format(**format_vars), str(mode)))
return args
def get_dbus_proxy_args(sb, bus):
args = []
if sb.get("dbus", {}).get("sloppyNames", {}).get(bus, False):
args.append("--sloppy-names")
for name, policy in sb.get("dbus", {}).get(bus, {}).items():
args.append(f"--{policy}={name}")
for rule_type in ("broadcast", "call"):
for name, rules in sb.get("dbus", {}).get("rules", {}).get(bus, {}).get(rule_type, {}).keys():
args.extend(f"--broadcast={name}={rule}" for rule in rules)
return args
def setup_dbus_proxy(sb, vars, proxy_dir, buses):
args = []
proxy_bwrap_args = ["--bind", proxy_dir, proxy_dir]
cmd_bwrap_args = []
for bus, address, addr_env in buses:
if bus_args := get_dbus_proxy_args(sb, bus):
args.extend((address, f"{proxy_dir}/{bus}", "--filter"))
args.extend(bus_args)
addr_path = address.removeprefix("unix:path=")
proxy_bwrap_args.extend(("--bind", addr_path, addr_path))
cmd_bwrap_args.extend(("--bind", f"{proxy_dir}/{bus}", addr_path))
if addr_env:
cmd_bwrap_args.extend(("--setenv", addr_env, address))
if not args:
return []
pr, pw = os.pipe2(0)
os.makedirs(proxy_dir, exist_ok=True)
args = ["xdg-dbus-proxy", f"--fd={pw}"] + args
logging.debug("proxy args for dbus proxy: %r", shlex.join(args))
if proxy_sb := sb.get("dbus", {}).get("sandbox"):
proxy_sb = get_sandbox(merge_sandboxes([proxy_sb]), vars)
debug_object("dbus proxy sandbox", proxy_sb)
proxy_bwrap_args = get_bwrap_args(proxy_sb) + proxy_bwrap_args
args = ["bwrap", "--args", str(pipefd("\0".join(bwrap_args).encode("utf-8")))] + args
logging.debug("bwrap args for xdg-dbus-proxy for bus: %s", shlex.join(proxy_bwrap_args))
if os.fork() == 0:
os.close(pr)
os.execlp(args[0], *args)
else:
os.close(pw)
assert os.read(pr, 1) == b"x"
return ["--sync-fd", str(pr)] + cmd_bwrap_args
def debug_object(label, obj):
if logging.getLogger().isEnabledFor(logging.DEBUG):
logging.debug("%s:\n%s", label, pprint.pformat(obj))
configs_sources = []
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--log-level", choices=[lvl.lower() for lvl in ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG")])
parser.add_argument("-n", "--name", action=tagged_append("name", configs_sources))
parser.add_argument("-j", "--json", action=tagged_append("json", configs_sources))
parser.add_argument("-f", "--file", action=tagged_append("file", configs_sources))
parser.add_argument("-s", "--set", action=tagged_append("set", configs_sources))
parser.add_argument("-a", "--autoload", action="store_true")
parser.add_argument("-M", "--no-match", action="store_true")
parser.add_argument("-D", "--no-default", action="store_true")
parser.add_argument("--default-sandbox", action="store", default="default")
parser.add_argument("executable", nargs="?")
parser.add_argument("args", nargs=argparse.REMAINDER)
args = parser.parse_args()
executable_name = os.path.basename(args.executable or os.environ.get("SHELL", "sh"))
if args.log_level:
logging.getLogger().setLevel(getattr(logging, args.log_level.upper()))
for global_path in (pathlib.Path.home() / ".config" / "sandbox.yaml", pathlib.Path.home() / ".config" / "sandbox.yml"):
if global_path.exists():
global_sandboxes = load_sandboxes_file(global_path)
configs = []
for source_type, source_data in configs_sources:
if source_type == "name":
for name in source_data.split(","):
configs.append(load_sandbox(name.strip()))
elif source_type == "json":
configs.append(json.loads(source_data))
elif source_type == "file":
configs.extend(load_sandboxes_file(source_data))
elif source_type == "set":
k, v = source_data.split("=", 1)
k = re.sub(r"^\$", "env.", re.sub("^:", "vars.", k))
v = json.loads(v) if v and (v[0] in '"[{'or v in ("true", "false", "null")) else v
obj = {}
cur = obj
for is_array, p, end in re.findall(r'(?:^|\.)(@?)("[^"=]+"|[^".=]+)(=$)?', f"{k}="):
p = p[1:-1] if p.startswith('"') else p
c = v if end else {}
if is_array:
cur[p] = [c]
cur = cur[p][0]
else:
cur[p] = c
cur = cur[p]
configs.append(obj)
else:
raise NotImplementedError
if args.autoload:
sb = try_load_sandbox(executable_name)
if sb and sb not in configs:
configs.append(sb)
if not args.no_match:
for sb in global_sandboxes:
if executable_name in sb.get("matches", []) and sb not in configs:
configs.append(sb)
if not configs and not args.no_default:
configs = [load_sandbox(args.default_sandbox)]
debug_object("configs", configs)
default_vars = {
"pid": os.getpid(),
"cwd": os.getcwd(),
"executable": args.executable,
"name": executable_name,
}
sb = get_sandbox(merge_sandboxes(configs), default_vars)
debug_object("sandbox", sb)
if sb.get("disableSandbox"):
os.execlp(args.executable, args.executable, *args.args)
dbus_proxy_dir = f"{os.environ["XDG_RUNTIME_DIR"]}/xdg-dbus-proxy/bwrap-{os.getpid()}"
dbus_proxy_args = setup_dbus_proxy(sb, default_vars, dbus_proxy_dir, (
("system", "unix:path=/run/dbus/system_bus_socket", None),
("user", os.environ["DBUS_SESSION_BUS_ADDRESS"], "DBUS_SESSION_BUS_ADDRESS"),
))
bwrap_args = get_bwrap_args(sb)
bwrap_args.extend(dbus_proxy_args)
logging.debug("bwrap command: %s", shlex.join(["bwrap"] + bwrap_args + [args.executable] + args.args))
os.execlp("bwrap", "bwrap", "--args", str(pipefd("\0".join(bwrap_args).encode("utf-8"))), args.executable or executable_name, *args.args)
@laur89
Copy link
Copy Markdown

laur89 commented May 10, 2026

Hi @sloonz !
Been following the parent gist thread and read the relevant excellent original blog posts some months ago.

My knee-jerk reaction to problems like these are always bash-first, but the more I've thought about it the solution has been converging with this very gist, so I've decided to nuke my original work and steal from you.

Copied this gist to haridusministeerium/bubblebox and have

  • fixed few minor bugs;
  • added typing to most vars & method signatures;
  • added bunch of TODOs with potential issues, questions & change ideas.

(broad-strokes changes are in commit messags)

Feel free to ignore, as it's non-trivial amount of questions, but I'd appreciate if you could take the time to answer the questions below, and TODO's sprinkled across bb script itself; think latter is best answered by opening a review on the file itself.

Questions:

  1. what's with 'assert False' -- just a shortcut to quick exception raising?
  2. what's with passing arguments to bwrap via --fd=FD file descriptor as opposed to via command line per usual?
  3. get_merge_policy(): what do kl & kg stand for? key-literal & key-glob?
  4. how is bwrap's --sync-fd used?
  5. is there a good reason why --autoload isn't the default?
  6. similar to 2) -- what's the purpose of xdg-dbus-proxy's --fd param?
  7. setup_dbus_proxy(): what's with

    assert os.read(pr, 1) == b"x"

does that signify the proxies are ready? where's "x" payload documented?

  1. -s/--set option parsing magic could use some documentation
  2. wouldn't it make sense to move the config keys from camel-case to kebab-case
    used by bwrap/xdg-dbus-proxy so there's no need for translation like what bwrap_name() does?
    in fact, is there even a real value in defining them as keys under sandbox config,
    as opposed to having list of say 'bwrapOpts' where we set the flags to pass through?
    that would also give us free validation by bwrap, as opposed to having to do
    it ourselves. note there absolutely is value in mounts and envs with shorter
    syntax and vars/env var expansion, but for simple opts... why overengineer?
    one potential issue with just-a-list-of-opts is how these options get merged,
    as we'd most likely just end up concating the lists.
  3. included seccomp example in the yaml here -- would love an explanation on how
    it was generated and which seccomp rule it provides.
  4. dbus config looks off to me. think it should be instead:
    dbus:
      system|user:
        sloppyNames: bool
        rules:
          broadcast|call:
            TODO
        policies:
            TODO

which means if config only defines say

    dbus:
        user:

...then user proxy is created with just the '--filter' flag.
this way there's clear separation between user & system bus configurations.

  1. why are "dbus.rules.*.*.*" merged by 'list' merge policy? can there be e.g. multiple --broadcast=org.freedesktop.portal.*= rules? My intuition tells it should be merged by 'literal', i.e. same rule names/keys should overwrite previous ones.
    EDIT: points 11-12 solved by a proposed solution on rework branch.

  2. is there a requirement to bind {env[XDG_RUNTIME_DIR]}/doc/by-app/{name} for portal? At least on my debian testing installation, we don't have write perms to create the dir:

      File "<frozen os>", line 228, in makedirs
    PermissionError: [Errno 1] Operation not permitted: '/run/user/1000/doc/by-app/cli'
    
    $ ls -l /run/user/1000/doc/ | grep by-app
    dr-x------ 2 laur89 laur89 0 Jan  1  1970 by-app/

EDIT: looks like it was a misunderstanding on my part. For portals to work, we need

  1. app name in flatpak-info to contain a dot (e.g. 'my.firefox' is ok, whereas 'firefox' or 'firefox.' are not) (fix);
  2. bwrapinfo.json to be written via bwrap's --info-fd option, also sorted on the rework branch (fix)

Thanks, and especially thanks for this script!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment