mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
Permission-gated step bridge for sandboxed mods
The sandbox blocks love.system and love.filesystem, which orphans the native step bridge (#452, #489): its one consumer can no longer call syncHealthSteps or read steps_pending.json (#1186). Adds a "steps" manifest permission (shown to the player like the others) gating a mod.steps facade: available() probes the bridge quietly, sync() forwards the async refresh, poll() hands the mod its copy of a delivery. The engine owns the pending file -- mods never name a path and receive only { steps, from, to }. Without the permission the acting calls name it, following the network gate. No new events, hooks or registries; nothing removed. RFC 0009. Tests: tests/modkit/cases/steps_bridge.lua (no-mod cold bridge, permissioned sync/poll, per-mod copies, contract-field filtering, malformed-delivery drop, unpermissioned refusal, bridgeless build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -528,3 +528,27 @@ local state, percent = mod.device:powerInfo()
|
||||
`"charging"`, or `"charged"`. `percent` is `0` through `100`, or `nil` when
|
||||
the platform cannot report it. The facade is read-only and does not expose
|
||||
URL launching, clipboard access, or other system operations.
|
||||
|
||||
## Real-world steps
|
||||
|
||||
On iOS and Android the game counts the player's real-world steps natively
|
||||
(HealthKit / the hardware step counter). A mod reaches that bridge through
|
||||
the `steps` permission in `manifest.json`, which the player sees in the
|
||||
mod manager like every other permission:
|
||||
|
||||
```lua
|
||||
if mod.steps:available() then
|
||||
mod.steps:sync() -- async; OS consent sheet on first use
|
||||
end
|
||||
-- later, at a quiet moment:
|
||||
local walk = mod.steps:poll() -- { steps = n, from = ?, to = ? } or nil
|
||||
```
|
||||
|
||||
`available()` is `false` on builds without the bridge (desktop) and for
|
||||
mods without the permission, so a probe is always safe. `sync()` asks the
|
||||
platform to refresh its count and returns whether there was a bridge to
|
||||
ask. `poll()` returns the next delivery for this mod — the engine consumes
|
||||
the native side's pending file itself, each permissioned mod receives its
|
||||
own copy of a delivery, and steps are anchored natively so the same walk
|
||||
is never delivered twice. Without the permission, `sync` and `poll` raise
|
||||
an error naming it.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# RFC 0009 — Permission-gated step bridge for sandboxed mods
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. Engine: `Steps.lua` (new), `Loader.lua`, `Manifest.lua`,
|
||||
`Sandbox.lua`. Test: `tests/modkit/cases/steps_bridge.lua`. Issue: #1186.
|
||||
|
||||
## Motivation
|
||||
|
||||
The iOS and Android builds count the player's real-world steps natively
|
||||
(#452, #489), exposed to Lua as `love.system.syncHealthSteps()` and
|
||||
delivered as `steps_pending.json` in the save-directory root. The sandbox
|
||||
correctly blocks both — `love.system` also launches URLs, and the file API
|
||||
names paths — but that leaves the bridge with no consumer: the mod that
|
||||
step counting was built for (Pokéwalker, steps→EXP) can no longer be
|
||||
written.
|
||||
|
||||
## The decision it extends
|
||||
|
||||
Extends the mod sandbox in `src/mods/Sandbox.lua` (blocked host modules
|
||||
stay blocked; legitimate operations receive narrow engine-owned facades)
|
||||
and the permission model `network` established: a `manifest.json`
|
||||
permission the player sees in the mod manager that genuinely gates a
|
||||
capability.
|
||||
|
||||
## The exact API delta
|
||||
|
||||
A new manifest permission token, `steps`, and a `mod.steps` facade:
|
||||
|
||||
- `mod.steps:available() -> boolean` — whether this build carries the
|
||||
native bridge. Answers `false` without the permission, so a probe stays
|
||||
quiet.
|
||||
- `mod.steps:sync() -> boolean` — asks the platform to refresh its count
|
||||
(async; the OS consent sheet still appears on first use, exactly as
|
||||
before the sandbox). `false` when there is no bridge.
|
||||
- `mod.steps:poll() -> { steps = n, from = iso?, to = iso? } | nil` — the
|
||||
next delivery for this mod, engine-consumed from the pending file. Each
|
||||
permissioned mod receives its own copy of a delivery.
|
||||
|
||||
Without the permission, `sync` and `poll` raise an error naming the
|
||||
missing permission, the way the network gate does. The engine owns the
|
||||
pending file: mods never learn its name or location, and only the three
|
||||
contract fields travel. No new event, hook, or registry names.
|
||||
|
||||
## Migration note for existing mods
|
||||
|
||||
Mods that called `love.system.syncHealthSteps()` and read
|
||||
`steps_pending.json` themselves add `"steps"` to `permissions` and switch
|
||||
to `mod.steps:sync()` / `mod.steps:poll()`. No other mod changes.
|
||||
|
||||
## Parity tests
|
||||
|
||||
- **No mod:** with nothing installed the bridge is never called and a
|
||||
pending file on disk is left untouched.
|
||||
- **Mod API:** a fixture mod with the permission syncs and receives a
|
||||
delivery through the public loader; two permissioned mods both receive
|
||||
the same walk; a second poll returns nil.
|
||||
- **No permission:** `available()` is false and the acting calls name the
|
||||
missing permission; the sandbox suite keeps proving direct
|
||||
`love.system` access is refused.
|
||||
- **Malformed delivery:** a bad or empty pending file is dropped whole
|
||||
rather than crashing a poll (the native anchor only advances on a
|
||||
successful sync, so nothing is lost).
|
||||
|
||||
## Deprecation etiquette
|
||||
|
||||
Nothing deprecated. The facade is additive; the sandbox's `love.system`
|
||||
and `love.filesystem` blocks remain in force.
|
||||
Reference in New Issue
Block a user