Commit graph

12 commits

Author SHA1 Message Date
Breadway
450454d164 Merge feature/capability-manifest (Workstream D) 2026-08-04 22:40:49 +08:00
Breadway
252f65593c Fix rules.toml integration test after merging Workstream A's manual-emit domain restriction
The test triggered its rules via IPC's no-source emit method with real
adapter-owned event names (bread.device.*, bread.power.*), which
Workstream A now correctly rejects as impersonation. Switched to a
custom non-reserved domain (testrule.*) since what's under test is
rules.toml -> bread.on() -> action wiring, not adapter-specific event
names -- real rules.toml usage is unaffected since it subscribes to
genuine adapter-sourced events, not manually-injected ones.
2026-08-04 22:40:41 +08:00
Breadway
3c70c7a823 Merge feature/rules-toml (Workstream E) 2026-08-04 22:38:05 +08:00
Breadway
0384ea1354 Merge feature/event-causality (Workstream B) 2026-08-04 22:33:29 +08:00
Breadway
6841163620 Add capability-scoped module API (Workstream D)
ModuleManifest gains a structured [[permissions]] field (bread-shared's
new ModulePermission/PermissionKind, shared between bread-cli and breadd
so the two never drift on what a permission "type" string means).

breadd now gives every third-party module's Lua chunk a scoped _ENV
instead of the shared global table: load_scoped_lua_file builds a fresh
`bread` table containing only baseline bindings (event subscription,
timers, json, module/store, logging, and the pure-Lua sugar built on top
of those) plus whatever the manifest's permissions grant, with a
metatable __index falling back to the real globals for everything else
(stdlib, require/package - so require("bread.devices") keeps working,
since builtins load unscoped and their closures capture that environment
lexically regardless of the caller's). _G is explicitly rebound to the
scoped table itself to close the obvious escape hatch. A module with no
manifest, or a manifest with no permissions key, keeps full ambient
access unchanged (today's behavior) but is now tracked as `ungated` in
module status and surfaced by `bread doctor`. An explicit `permissions =
[]` is scoped for real but not flagged, since that's a deliberate
declaration.

Adds `bread modules audit <name>`: a best-effort text scan of a module's
.lua files suggesting a [[permissions]] block to paste into its manifest.

Converts examples/modules/cpu-temp-widget.lua into a directory module
with a worked bread.module.toml (fs.read + widget) as the reference
example. Documentation.md gets a new "Capability-scoped modules" section
covering the taxonomy, the require()/closure mechanism, and an explicit
note that path/bin scoping is recorded but not yet enforced per-call -
that's the out-of-process module sandboxing workstream this manifest
schema is laid down for. API_VERSION bumped 1.4.0 -> 1.5.0.
2026-08-04 22:24:16 +08:00
Breadway
45b5aee117 Add rules.toml: declarative event->action automation without Lua
Adds a bread.rules built-in module plus a breadd/src/core/rules.rs
parser/validator so the common "when event X happens, do Y" case
(dock connect script, AC-disconnect notification, keyboard rate on
connect) no longer requires hand-written init.lua.

- rules.toml is optional, XDG_CONFIG_HOME-aware (mirrors breadd.toml's
  config_path() resolution), and purely additive alongside init.lua.
- Each [[rule]] needs `on` (event suffix, "bread." implied, wildcards
  supported) and exactly one of run/exec/notify. `run` names a single
  script (tilde-expanded + shell-quoted so spaces in the path can't be
  word-split); `exec` is a raw shell command line passed through as-is;
  `notify` shows a desktop notification.
- Rule data is threaded into the bread.rules Lua module via globals
  set just before it loads (same technique load_profiles() already
  uses for __profiles_path), avoiding any need to hand-escape values
  into generated Lua source text.
- Parse/validation failures surface through the existing module
  load-error path (Lua error() -> run_on_load -> set_module_status),
  so a bad rules.toml shows up via `bread doctor` exactly like a
  broken hand-written module would, without blocking other valid
  rules in the same file.
- Documentation.md gets a new Getting-started fast path plus a
  Dictionary entry; README's Configuration section gets a short
  pointer. Since: v1.5.
2026-08-04 22:05:26 +08:00
Breadway
6ff1ee910b Add event causality tracking (id + caused_by chains)
Every BreadEvent now gets a unique id at construction (via
BreadEvent::new/with_timestamp, and the normalizer's struct-literal call
sites, which all now assign one explicitly). A new caused_by field is
populated only when a Lua bread.emit() call runs synchronously inside a
bread.on subscriber's handler: LuaEngine tracks the currently-dispatching
event's id (set/restored around each handle_event invocation, single-
threaded so a plain Mutex-guarded slot suffices) and bread.emit()'s Rust
binding reads it when constructing the outgoing event. This lets chains of
Lua modules that react to each other's events be reconstructed instead of
timestamp-guessed from a live `bread events` log.

Also:
- bread events --tree renders the caused_by chain as an indented,
  live-streaming-friendly tree instead of a flat line-per-event stream.
- API_VERSION bumped 1.4.0 -> 1.5.0 (additive-only change).
- Documentation.md's event envelope, bread.emit, and debugging-tips
  sections updated with Since: v1.5 markers.
- New end-to-end regression test spawning the real daemon with 3 chained
  Lua handlers (A emits X on trigger, B emits Y on X, C emits Z on Y) and
  asserting caused_by threads correctly through all three hops.
2026-08-04 17:55:31 +08:00
Breadway
7bb6fbb20f ipc: close unauthenticated event-spoofing gap in emit's no-source path
The IPC "emit" method's no-source path took a bare event+data and sent it
straight to emit_tx tagged AdapterSource::System with zero validation of
the event name. Any same-UID process on the socket could send e.g.
{"event":"bread.power.ac.connected",...} and have it delivered to every
Lua subscriber indistinguishable from a real adapter event, since System
is the same tag the daemon uses for its own trusted, Rust-originated
sends (bread.system.startup, bread.profile.activated).

Fix, scoped to the actual threat model (same-UID Unix socket trust means
there's no way to cryptographically distinguish "the real bread-cli
binary" from any other local process, so a generic connection-identity
handshake would be theater):

- New AdapterSource::Manual tag for the no-source emit path. System is
  now reserved for daemon-internal, Rust-code-originated sends only and
  can never again be produced from data that arrived over the wire.
- The event name is rejected if its top-level dotted segment is one of
  the reserved, adapter-owned domains (RESERVED_DOMAINS in
  bread-shared/src/apps.rs) -- extended with bluetooth/workspace/window/
  monitor, event families the Hyprland and Bluetooth adapters already
  publish under but that were missing from that list. Freely-named
  custom/test event names are untouched, so `bread emit <name>` and
  bread-emit's fire-and-forget single-line-write design keep working
  exactly as documented.

Bumped API_VERSION to 1.5.0 and updated Documentation.md's IPC emit
section and Namespaces reserved-domains list accordingly.

Also fixed a subscribe/emit race that surfaced while adding regression
tests for this: events.subscribe's ack is written to the client before
the server task actually registers on the broadcast channel, so a test
that emits immediately after reading the ack can race the registration.
Added a settle delay plus an explicit timeout (instead of an unbounded
read loop) so a future regression fails the test instead of hanging the
whole binary.
2026-08-04 17:41:18 +08:00
Breadway
1208c5d1b7 Add filesystem/git/podman/systemd adapters, git/shell hooks, bread-emit CLI, app-detection helpers 2026-07-19 03:05:57 +08:00
Breadway
cc456b78fe refactor: remove remote module install, extract bread-sync, make CI real
Security:
- Remove `bread modules install github:…`. Remote fetch pulled unreviewed
  third-party Lua and ran it with full bread.exec() privileges in an
  unsandboxed runtime. Module install is now local-only; parse_source
  rejects github:/git: with an explicit message.

bread-sync extracted from the workspace (parked for its own project):
- Removed from workspace members (now excluded); see bread-sync/EXTRACTION.md
- Removed the entire `bread sync` CLI surface and now-unused deps
  (bread-sync, reqwest, tar, flate2; tempfile demoted to dev-dependency)
- Removed the sync.status IPC method from breadd plus its integration tests
- Moved the generic `expand_path` helper into bread-shared (with unit tests)

CI now actually runs and gates quality:
- Trigger on master/dev (was `main` — CI had never run, not once)
- Added `cargo fmt --check` and `clippy -D warnings`; fixed 4 clippy warnings
- Dropped the macOS matrix entry (breadd is Linux-only: udev/rtnetlink);
  added the libudev-dev system dependency the Linux build needs

Hardening / honesty:
- New ipc test: daemon survives repeated reloads and the event pipeline
  resumes (the prior suite only had a single happy-path reload check)
- Docs scrubbed of sync across README/Documentation/Overview/DAEMON
- "production-ready" and "compositor-agnostic" claims reworded to match
  reality rather than aspiration

Note: bread-sync/src/export.rs held pre-existing local WIP authored outside
this change set and is intentionally excluded from this commit.
2026-05-17 00:22:21 +08:00
Breadway
9a471f3158 Final Release of Version 1.0 2026-05-13 22:01:42 +08:00
Breadway
730a8b61d7 Release 1.0 2026-05-11 11:56:03 +08:00