Permissions and Security
Model
A plugin declares capabilities in its manifest. The Host exposes a fixed set of operations; each operation maps to a permission:
| Permission | Operations |
|---|---|
player.read |
player.getPosition |
world.read |
world.getScene |
entities.read |
entities.list |
map.read |
map.getMarkers |
teleport.write |
teleport.to |
ui.create |
ui.setLayout, ui.update |
notifications.show |
notifications.show |
storage.read |
storage.get, storage.keys |
storage.write |
storage.set, storage.remove |
events.subscribe |
events.subscribe, events.unsubscribe |
game.invoke |
game.functions, game.invoke |
api.info, plugin.hello, plugin.ping and logging.log require no
permission; logging is rate-limited and capped.
The user approves permissions per plugin in the Plugin Manager. Approval is not
a one-time install flag: the Host checks the granted set on every call. A
revoked permission fails immediately with -32000.
Isolation
- Separate process. A plugin is launched as a child process. No plugin DLL is loaded into the Shire process. A crash or hang closes that plugin's pipe and cannot directly corrupt the Core.
- Job object. Each child is assigned to a job with a 256 MiB process-memory
limit and
KILL_ON_JOB_CLOSE; stopping the plugin terminates the job. - Restricted token. The Host tries
CreateRestrictedTokenwithDISABLE_MAX_PRIVILEGEandCreateProcessAsUserW. When that is available the child does not retain the game's administrative privileges. If the environment cannot create the token/process, the Host falls back toCreateProcessWand records the fallback in the plugin log; the capability boundary remains, but the OS token is inherited. - No native handles. The JSON-RPC contract contains no game pointers, offsets, HMODULEs, file handles or ImGui context. Entity ids are opaque hex strings; the Host resolves them internally.
- Package isolation.
.shiremodextraction rejects absolute paths,.., drive colons and deflated entries, and caps entry/total size. A package is extracted to a staging directory before it replaces an existing plugin.
Input validation
Every handler validates type, length, range and finite numbers before touching
the Core. UI layouts are capped (64 components, 16 KiB text). Storage keys are
capped at 128 bytes and total storage at 256 KiB. Notifications are capped.
Unknown methods return -32601; unknown permissions are rejected by manifest
validation.
Rate limits and queues
- 300 calls/second per plugin, including a burst; excess is
-32001. - Inbound queue: 2,048 lines; a flooding connection is closed.
- Outbound event queue: 256 lines per connection; oldest dropped.
- Pipe writes have a timeout; a plugin that stops reading is disconnected instead of blocking the Host forever.
- The Host processes at most 32 inbound lines per frame and does not call the Core when no plugin is subscribed to an event.
What is not guaranteed
No client-side system can guarantee secrecy against a user who controls the machine. A determined user can inspect the plugin process, the payload, the pipe traffic or the Windows APIs. The design minimizes unnecessary exposure:
- the SDKs never contain offsets or private code;
- the Host sends only public copies;
- the plugin process is not in the Core address space;
- approvals are explicit and per-call.
A plugin process is still a normal OS process. It can open files and sockets that the OS permits; the permission system governs the Shire API, not the whole operating system. If a future release needs stronger network/filesystem isolation, an AppContainer or a separate low-privilege service is the next step.
Never put administrative secrets (Cloudflare tokens, Discord bot tokens, license signing keys, client HMAC secrets) in a manifest, package, SDK or log. The SDKs contain none, and the Host never sends them over the pipe.