dsh-pet
Verified@linxin666/dsh-pet Β· v0.3.5 Β· Apache-2.0 Β· Web UI
Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score
Install
dsh plugin add @linxin666/dsh-pet Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Readme
dsh-pet β Multi-pet companion plugin
English | δΈζ
A registry-driven desktop companion for DeepSeek Harness β the built-in whale girl plus any pet you drop in.
While the model thinks, you wait β your pet swims. It follows official session activity and switches animations while waiting, thinking, using tools, composing a reply, celebrating completion, or reporting failure; you can also pat its head, feed it dried fish, and watch its affinity grow. Pets are registry entries, not code: every pet is one pet.json manifest plus one atlas image, and the host discovers them at startup.
Re-implemented from the pet feature of the Codex desktop app, as an official DSH plugin shape (cordis bundle: host half + client half in one package).
Features
| Feature | Description |
|---|---|
| Multi-pet registry | The host scans built-in assets/, the hatch-pet custom pets directory, and composed config entries; each pet is a manifest plus an atlas |
| Pet selection in settings | The plugin settings card lists every registered pet (built-in assets plus user directories β the installed set); switching persists and the sprite swaps immediately. The card sits in the first-level Pet settings section |
| Per-pet naming | Rename from the hover panel; each pet keeps its own name (stored per pet id, migrated from the legacy flat name) |
| State animation | Official session activity β manifest-defined sequences of 9-state tracks; each track finishes its full duration before the sequence advances and the complete sequence loops |
| Head-pat interaction | Click the pet β bubble feedback + affinity +1 (10s cooldown) |
| Feeding | Hover panel ει£ (Feed) β consumes 1 dried fish + affinity +5 (30s cooldown) |
| Treat economy | Dried-fish stock (cap 20): +1 every 30 rounds of work, +1 every 300 minutes (5 hours) β 10x rarer than the original cadence |
| Affinity | +1 per round completed; 9 levels: εΉΌι²Έ β δΌδΌ΄ β ζε β ζ·±ζ΅·ηΎη» β εΏζη΅η β δΌ θ―΄ηΎη» β η₯θ―ηΎη» β ζ°ΈζδΉε₯ β ι²Έηε ±ζΈ‘ (capped at 999,999,999) |
| Dragging | Hold and drag the pet to reposition; position persisted |
| Hide/Summon | The hover panel sits below the pet (lifted above the status bubbles when there is no room below) and provides ιθ (Hide); after hiding, a ε¬ε€{name} (Summon {name}) button appears |
| Witty remarks | Built-in remark library (10 lines per event) plus per-pet custom lines; success lines rotate by persisted success counts and cooldown lines by persisted rejection counts |
| Status bubbles | Only one top-level session speaks by default β the session the GUI is currently on when it is reported, otherwise the most recently active one β and the rest collapse behind a +N badge on the main bubble instead of stacking a tall column; hover the bubble (or tap the badge, for touch) to fan every session's bubble out above it and click one to jump to its session; subagent sessions report through their spawning conversation and never occupy a bubble of their own; transient interaction feedback temporarily takes priority. Bubble copy comes from generous rotating pools per scene (waiting / thinking / writing / done / failed...), tool calls map onto per-family witty lines carrying the real argument hint (e.g. θ·θ· npm test), and a long-lived scene re-phrases itself every few seconds |
| Inner whispers | η’η’εΏ΅: while a session streams, the pet occasionally speaks its inner voice through that session's own bubble β a fresh whisper takes over the bubble and marks it with γγ quotes β sharing the same DeepSeek-blue glass as every status bubble, so stacked bubbles never clash β instead of stacking a second bubble β category lines woken by the SITUATION (thinking / writing / the running tool family) plus outcome lines woken only by structured results (test green from a passed test tool, error from a failed tool result, completion from a completed turn) β never from the model's words, so a discussion that merely mentions a keyword cannot wake a mood, and a whisper never quotes real content (no tool names, paths or model text); paced by cooldowns (9 s category, 5 s outcome) and an 8 s display TTL, the status copy returns after a few seconds |
| Multi-session activity | The pet is host-global: the most recent meaningful event drives the sprite animation while every active top-level session reports its own state in a separate bubble; completed turns from every session (subagents included) contribute affinity and treats |
| Voice packs and panel DIY | A per-pet voice.json plus the global $DSH_HOME/pets/.voice.json override replace every bubble word and the hover panel (button labels, stat formats, button visibility); merge precedence per-pet > global > built-in, broken packs warn and never reject a pet |
Pet contract
A pet is a directory holding one pet.json manifest and one atlas image. Nothing else is required β no host or client code changes.
{
"id": "whale-girl", // unique lowercase kebab id
"displayName": "ι²Έι±Όε¨", // shown in the settings selector and panel
"description": "A soft healing whale-girl.", // optional
"spritesheetPath": "spritesheet.webp", // atlas, relative to the manifest
"cell": { "width": 192, "height": 208 }, // optional; defaults to the Codex contract
"columns": 8, // optional; default 8
"spriteVersionNumber": 1, // optional; 2 marks an 11-row v2 atlas (9 animation rows + 2 look rows)
"frames": [6, 8, 8, 4, 5, 8, 6, 6, 6], // optional per-row frame counts
"tracks": { // optional per-track rhythm overrides
"idle": { "durations": [400, 400, 500, 400, 400, 500] }
},
"sequences": { // optional per-scene track sequences (at least 5 items each)
"thinking": ["running", "running-right", "running", "running-left", "waiting"]
},
"remarks": { // optional witty remarks (one line or a pool per slot)
"pet": "ζΈζΈζ°΄ηη倴ο½",
"feed": ["ε°ι±ΌεΉ²ηι¦", "εζ₯δΈζ‘ο½"]
}
}
- The atlas is an 8-column Γ 9-row grid (192Γ208 cells by default); rows are fixed in this order: 0 idle, 1 running-right, 2 running-left, 3 waving, 4 jumping, 5 failed, 6 waiting, 7 running, 8 review. Unused cells stay fully transparent. v2 Codex atlases declare
"spriteVersionNumber": 2and hold 11 rows β the same 9 animation rows plus 2 trailing look rows; the plugin renders the 9 animation rows and ignores the look rows. - The optional remarks block overrides the reaction bubbles the pet speaks on pet / petCooldown / feed / feedCooldown / noTreats events. Each slot accepts one line or a pool of lines; a declared slot replaces the built-in pool for that slot only. Success and cooldown pools use the corresponding persisted success or rejection count, while noTreats cycles independently. This is how community contributions give their pet its own witty voice.
framescounts the used columns per row (defaults to the hatch-pet contract table[6, 8, 8, 4, 5, 8, 6, 6, 6]);tracksoverrides per-frame durations (cycled to the row's frame count),loop, andfallbackper animation (defaults: everything loops;jumpingandfailedhold their last frame, then fall back toidle).sequencesoptionally maps activity scenes (idle/waiting/thinking/tool/review/done/failed) to at least 5 animation tracks. Each item plays every frame for the durations intracks, then the next item starts; the complete sequence loops. An omitted scene keeps its canonical single-track playback.
Manifest v2 (pet center, #623)
A pet directory's pet.json declares its renderer explicitly in v2:
petManifestVersion: 2(absent = v1, compat-read assprite2dwith a migration hint);renderer:"sprite2d"(the atlas contract above),"live2d", or"frames2d";license(required in v2): asset license identifier β community pets carry provenance;- a renderer block:
sprite2d(spritesheetPath/cell/columns/atlasRows/frames/tracks),live2d(model/motions/expressions/hitAreas/scale/translate), orframes2d(dir/defaultFrameMs/tracks/phases β directory-style frame sequences).
Validation is fail-closed on structure (unknown fields or renderer kinds reject the entry with a diagnostic) and warn-and-drop on sequence/remark content. The machine-readable schema lives at contracts/pet-manifest-v2.schema.json; the authoritative validator is src/manifest-v2.ts. Migrate v1 manifests with node scripts/dsh-pet-migrate-v2.mjs <dir> --write (dry-run by default; keeps pet.json.v1.bak).
Where pets come from (later sources override earlier ones on id collision):
- Built-in:
assets/<dir>/pet.jsonin this package. - Legacy custom pets:
${CODEX_HOME:-~/.codex}/pets/<pet>/pet.jsonβ the hatch-pet pipeline stages its output there, so a hatched pet appears in the selector with no further wiring. - Pet-center user directory:
$DSH_HOME/pets/<id>/β the recommended home for your pets (see the CLI below). - Composed:
PetConfig.petsmanifest entries passed to the plugin by the embedding application.
Validate and install a pet directory with the CLI (no build step, no npm publish):
node scripts/dsh-pet validate <dir> # manifest + assets + Live2D reference closure + voice.json
node scripts/dsh-pet install <dir> # validate, then copy into $DSH_HOME/pets/<id>/
node scripts/dsh-pet install <dir> --force # overwrite an existing same-id install
Invalid entries never override a working pet: they are skipped with a diagnostic listed in the settings (Pet section). The registry is built once at host startup; add or change a pet, then restart dsh web.
Voice packs and panel chrome (voice.json, pet-center M4, #677)
Every word in the thought bubble (status / tool / whisper copy) and the hover panel (button labels, stat formats, button visibility) can be replaced by a pet β or by you β without touching plugin code. Pets ship an optional voice.json in their directory; a global override at $DSH_HOME/pets/.voice.json re-voices pets without editing their directories.
{
"voicePackVersion": 1, // optional; absent reads as v1
"status": { // status pools keyed by scene id; per-key override
"done": ["Done for today!", "Another one down"]
},
"tools": { // tool pools keyed by tool family; {tool} / {hint} allowed
"shell": ["Running {hint}", "Hit enter: {hint}"]
},
"toolRemaining": ["{n} helpers still at work"], // {n} allowed
"whispers": { // murmur pools; keys replace the built-in pools
"categories": { // situation pools; an explicit empty array mutes that category
"thinking": ["Let me think..."],
"running": ["It's running now"]
},
"results": { // outcome pools (test green / error / completion)
"pass": ["All green!"]
}
},
"panel": { // hover panel; unset slots keep the plugin i18n copy
"labels": { "feed": "Treat", "hide": "Dive", "rename": "Rename me", "confirm": "Sure" },
"stats": { "rank": "Affinity {rank}", "treats": "Treats x{n}", "points": "{points} pts" },
"actions": ["feed", "rename", "hide"] // subset in canonical order; absent = all; [] = stats only
}
}
- Merge precedence (per slot): the pet voice.json > the global .voice.json > built-in copy. status/tools/whispers merge per key, panel merges per slot; any slot a layer misses falls through.
- Placeholder whitelist: tools accept {tool} / {hint}; toolRemaining accepts {n}; panel.stats accept {rank} / {n} / {points}; status, whisper and panel-label lines accept no placeholders (lines carrying one are dropped with a warning).
- Caps (warn-and-drop): at most 64 lines per pool and 160 characters per line; panel labels 40 and stats 80 characters.
- A broken pack never breaks the pet: voice.json that is not valid JSON or whose root is not an object is ignored with a warning; every other issue drops its slot only. Diagnostics appear under Settings > Pet directory diagnostics. node scripts/dsh-pet validate fails installs on structure errors and lists content issues as warnings.
- Semantics: an empty status/tools pool falls back to the built-in copy (a scene line always renders); an explicit empty whisper pool mutes that channel; an empty panel actions array hides all three buttons; uncovered buttons and stats keep the plugin bilingual dictionary. Legacy whispers.generic / whispers.rules fields are no longer supported and are ignored with a warning.
Live2D pets (renderer: live2d)
Live2D pets render through PixiJS/WebGL: the MIT pixi.js + untitled-pixi-live2d-engine stack ships inside this plugin as a lazily loaded vendor bundle, so sprite-only installations never download or parse it. The Cubism Core runtime is never bundled or downloaded by this plugin β Live2D's proprietary license forbids redistributing it. To enable a Live2D pet:
- Obtain the official Live2D Cubism SDK for Web yourself (you accept Live2D's license) and take
live2dcubismcore.min.jsfrom it. - Place it at
$DSH_HOME/pets/.runtime/live2dcubismcore.min.jsβ the plugin serves it to the page from there, alongside its own vendor bundle. - Install a Live2D pet (a directory with
pet.jsonv2,renderer: "live2d", and the model files).
If the core is absent, a Live2D pet shows an install-guidance card where the model would render; sprite2d pets are unaffected. Legal note: this plugin is an "extensible application" in Live2D's terms β works you publish with user-loadable models may require a Live2D release license regardless of scale; evaluate your obligations before publishing derivative works.
A Live2D manifest maps the seven activity phases onto the model's motion groups:
{
"petManifestVersion": 2,
"id": "my-live2d-pet",
"displayName": "My Live2D Pet",
"license": "CC0-1.0",
"renderer": "live2d",
"live2d": {
"model": "model/my-pet.model3.json",
"motions": { "idle": "Idle", "thinking": "Think", "failed": "TapBody" },
"hitAreas": ["Body"]
}
}
model: the.model3.jsonpath relative to the pet directory. Every file the model references (moc, textures, motions, physics, pose, expressions) must live inside the directory β the host serves exactly that reference closure.motions(required,idlemandatory): phase β motion group. Unmapped phases and groups the model lacks fall back toidle; a group holding several motions plays a random one. The official Cubism sample models ship onlyIdleandTapBodygroups.expressions(optional): phase β expression name, layered over the motion.hitAreas(optional): a tap landing on a listed hit area plays the model'sTapBodygroup, then returns to the phase's group. Every tap still counts as petting β the chrome owns interactions exactly like sprite2d.scale/translate(optional): the model auto-fits the display box;scalemultiplies the fit (default 1, range (0, 10]) andtranslateoffsets it in px from the center.
Model licensing: the official Live2D sample models (Hiyori, Haru, and friends) are evaluation-only and must not be redistributed β ship only models you have rights to (original creations or permissively licensed ones).
Frames2d pets and gameplay (renderer: frames2d)
Frames2d pets ship directory-style frame sequences instead of an atlas: thumb/<track>/<frame>.webp, with per-frame durations from an optional _<ms> filename tail or the track's frameMs list (default 200 ms, bounds 16β5000). The manifest maps the activity phases onto tracks; a drag track follows the chrome's drag gesture, and non-looping tracks settle into their fallback (default the idle track), so intro/loop splits (sleep-intro β sleep) are plain manifest data.
A frames2d pet may declare a gameplay block β an opt-in mini-game layer generalized from the miku desktop pet: decaying stat bars (stats with per-minute decay, a working variant and an idle variant), the unified treat (ε°ι±ΌεΉ²) currency β gameplay income and shop spending ride the same panel treat stock (capped at 20): work success, passiveIncome and lottery prizes grant treats, and shop items are priced and paid in treats, with no separate wallet page β a weighted idleDirector (rolls an act every intervalMs, maxMiss forces one after consecutive idle rolls), touch zones inside a hitBox (roll branches with effects, a track hold and phrase bubbles), a work loop (host-adjudicated ticks with success/fail result tracks), a sleep loop (lazy stat restore), passiveIncome, and a shop whose items carry effects or tiered lotteries. All rolls and bookkeeping are host-authoritative (POST /api/pet/gameplay/*); state persists per pet in pet.json and settles lazily on the treats-economy discipline. The browser half renders the menu card (stat bars, work/sleep toggles, shop grid) automatically for any pet that declares the block.
The Miku pet (contributed by stushansusu under MIT; Hatsune Miku character rights belong to Crypton Future Media under the Piapro Character License β see THIRD_PARTY_NOTICES.md) is the reference frames2d gameplay pet. It ships through the Workshop only (not the npm bundle): install it from the Workshop's pet list and it lands in $DSH_HOME/pets/miku/.
Status decorations (decoration.json, pet-center M5, #567)
A status bubble can carry a small ornament ahead of its text (built-in: the spouting whale), driven by the ActivityPhase stream. Decorations are independent of pets: own descriptor, own id, own directory β switching pets never switches decorations. Entry assets are PNG/WebP single-row sprite strips only (no SVG/CSS); the bubble always keeps its role=status/aria-live (or session-bubble button semantics), the ornament is aria-hidden; prefers-reduced-motion holds the segment first frame, and a broken asset only removes the ornament β the text stays.
{
"decorationManifestVersion": 1,
"id": "whale", // unique lowercase kebab id
"displayName": "Spouting whale", // optional
"license": "MIT", // required: asset provenance
"entry": "whale-frames.png", // PNG/WebP strip, relative to this directory
"cell": { "width": 64, "height": 48 },
"columns": 4, // strip frames (1..16)
"frameMs": 160, // constant frame duration; or "durations": [..] per frame
"loop": true,
"phases": { // ActivityPhase -> inclusive frame segment; "hide" = none; default hide
"idle": "hide",
"waiting": { "from": 0, "to": 1 },
"thinking": { "from": 0, "to": 3 },
"done": { "from": 2, "to": 3 },
"failed": { "from": 3, "to": 3 }
}
}
- Structure is fail-closed (unknown fields, out-of-range geometry, non-PNG/WebP entries reject with diagnostics); segment content is warn-and-drop. The machine-readable twin lives at contracts/status-decoration-v1.schema.json; the authoritative validator is src/decoration.ts.
- Sources: built-in assets/decorations/ plus the user directory $DSH_HOME/pets/decorations// (same id overrides the built-in). Assets ride /api/pet/decoration// with the same containment and allow-lists as pet assets.
- Switch: Settings > Pet > Status decoration (on by default). The built-in whale derives from the DeepSeek wordmark (MIT); see THIRD_PARTY_NOTICES.md.
Built-in pets
| Registry id | Selector label | Source |
|---|---|---|
ouo-neko |
OUO Neko | Pink-sakura cat-eared companion contributed by Pessimist0906 under MIT |
whale-girl |
ι²Έι±Όε¨οΌεηοΌ | The repository's original whale-girl atlas |
whale-girl-refined |
ι²Έι±Όε¨οΌη²Ύθ΄ηοΌ | An AI-assisted derivative with repaired and refined details, based on the whale-girl design direction |
The Miku pet is deliberately not bundled: it is a frames2d gameplay pet installed on demand from the Workshop (see the frames2d section above).
The refined variant references DreamSkin's βDeepSeek-Whaleβ theme. The historical source record identifies powerdog996 as the original theme author and marks the theme as MIT: DreamSkin, repository source record. This attribution records the source and derivative relationship; it does not present the refined variant as an official work of the original author or redefine the original artwork's licensing scope.
Animation preview
Sprite pets use 8-column atlases with 192Γ208 cells generated by the hatch-pet pipeline. Classic atlases carry 9 animation rows; v2 atlases add 2 rows for 16 look directions. Below are previews of the standard animation states:
| idle | waiting | running | jumping |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| waving | review | failed | move left/right |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Architecture
dsh-pet/
|-- src/
| |-- index.ts # host half: plugin entry (registry build, settings section, routes)
| |-- registry.ts # multi-pet contract: manifest scan + normalization (assets + custom pets)
| |-- service.ts # PetService: pet selection + state machine + affinity + config
| |-- state.ts # pet state machine: projected session activity β 9 state animations
| |-- remarks.ts # witty-remark library: built-in pools + per-pet overrides + counted picker
| |-- affinity.ts # affinity ledger (pure functions + cooldowns)
| |-- treats.ts # dried-fish stock ledger
| |-- persist.ts # persistence ($DSH_HOME/pet.json: selection + names + interaction counts)
| |-- routes.ts # /api/pet/* JSON API + /pet/<id>/* asset routes
| `-- client/ # browser half
| |-- index.ts # global mount (createRoot β body) + registry fetch + polling + wiring
| |-- PetDockEntry.tsx # global floating entry (document.body, always shown)
| |-- PetSprite.tsx # definition-driven floating sprite (portal + rAF + dragging)
| |-- PetSettingsCard.tsx # settings card: pet selector + display layout
| |-- sequences.ts # full-track scene sequence timing
| |-- spritesheet.ts # atlas geometry helpers + track trimming
| `-- pet.module.css
|-- assets/whale/ # built-in original whale-girl (manifest + atlas + previews)
|-- assets/whale-refined/ # built-in refined whale-girl registry variant
|-- assets/ouo-neko/ # built-in OUO Neko v2 pet (11-row atlas + previews)
`-- cordis.patch.yml # bundle patch: inserts the pet plugin row
Data flow
official session events (turn/step/chunk/tool) ----\
> PetService (host) <-- registry (assets + custom pets)
optional legacy activity/status ------------------/
| /api/pet/* JSON
global React root (createRoot β document.body) <-- polling 2s -- pet-client (browser)
|
PetSprite floating layer (portal + rAF)
- Status source: the host projects official
turn/start,step/start,assistant/chunk,assistant/message,tool/call,tool/result, andturn/endevents into waiting/thinking/tool/review/done/failed states. Optional legacyactivity/statusevents remain a compatibility input. - Registry: the host normalizes every manifest into a full render definition (geometry, per-row frame counts, per-track durations) and serves it over
/api/pet/pets; the browser half renders any entry from that definition and carries no per-pet code. - Selection & naming:
petIdlives in the settings namespace; per-pet names live inpet.jsonundernames, edited through the hover-panel rename of the active pet. Legacy installs migrate their flatnameonto the whale girl. - Multi-session semantics: the API and browser mount are host-global and expose no foreground-session identity. Concurrent sessions each keep their own projected state: the most recent meaningful event drives the sprite animation, while every active TOP-LEVEL session reports its stage in its own bubble (the state view's sessions list, capped at 12 most-recent). Subagent children are tracked for animation, rewards, and the single display bubble but render no bubble of their own, so N conversations never multiply into an N-plus-subagents stack. Every session's completed turns are still rewarded independently; disposing a session removes its bubble, and disposing the display session falls back to the most recent remaining one.
- Mount point:
document.body(global React root, always shown: no session / new session / mid-session β the old mount pointconversation.composer.dockonly rendered in an active session, hiding the pet in new sessions); the component usescreatePortalinternally to render the global floating layer. The root follows the plugin fiber lifecycle: fiber disposal unmounts the React root, removes the container, and stops the poll loop and settings subscription; a hot-reloaded or re-injected bundle takes over the page-global slot, so the page always holds exactly one[data-dsh-pet-root](issue #785). - Rendering: CSS sprite (background-position) per-frame animation; frame durations and optional scene sequences come from the served definition. The hover panel is anchored below the pet with a pointer bridge across the gap; when the viewport leaves no room below, it flips above the pet and is lifted clear of the status bubble stack so the two never overlap.
- Communication: browser β host over the same-origin
/api/pet/*JSON endpoints (state/pets/interact/set-visible/set-config/set-name/set-pet); each pet's atlas loads from/pet/<id>/<spritesheetPath>β the plugin self-sufficiently provides its own API and assets (the same pattern as dsh-remote-web-ui's/api/pair).
Install
Install the family aggregate package @linxin666/dsh-web-all (all plugins and skins in one) or this plugin alone:
### From npm (recommended)
dsh plugin --profile web add @linxin666/dsh-pet@latest
### From the repository (development)
git clone https://github.com/zhu1090093659/dsh-web.git
cd dsh-web
pnpm install && pnpm -r build
dsh plugin --profile web add link:$(pwd)/packages/dsh-pet
After installing, restart dsh web β your selected pet appears at the bottom-right of the interface. In link mode, pnpm build and refresh the page after a code change; no reinstall needed.
Development
pnpm build # tsc -b (types+declarations) && tsdown (node half + browser bundle)
pnpm test # vitest unit/component tests (registry / event projection / state / UI / ledgers)
pnpm prepare # transpile-only build (no type checking, for consumer installs)
pnpm typecheck # type check only
The browser bundle rides the window.__ModuleLoader__.load contract; React/cordis and so on resolve from the loader's module table (external); CSS Modules are inlined by lightningcss as <style data-plugin>.
Sprites and animation-track calibration
The two built-in whale-girl atlases use the same 9-state Γ 8-column contract: assets/whale/ is the original and assets/whale-refined/ is the refined variant. Each atlas is 1536Γ1872 (8 columns Γ 9 rows of 192Γ208 cells). Frame counts, rhythm, and scene rotation live in each directory's pet.json; pets without overrides follow the hatch-pet contract rhythm and canonical single-track scene mapping (row order: 0 idle / 1 running-right / 2 running-left / 3 waving / 4 jumping / 5 failed / 6 waiting / 7 running / 8 review).
OUO Neko uses the extended v2 contract: a 1536Γ2288 atlas with the same 9 animation rows plus 2 eight-frame look-direction rows. Its manifest declares sprite2d.atlasRows: 11, and the renderer treats the last 16 cells as the clockwise look loop.
Security model
- Every
/api/pet/*and/pet/<id>/*route is loopback-only by default (the shared plugin-family fence: loopback socket + Host header + browser same-origin markers): unpaired LAN clients get403 forbidden: loopback-onlybefore any pet state or atlas is served. Whendsh-remote-web-uiis also loaded, a live paired-device cookie is an additional allow path (the same cookieapi/gatealready checks); unpaired and revoked devices stay 403. The pet does not depend on the remote plugin. - Asset serving resolves both the pet directory and the candidate file through
realpath; symlink escapes are refused (403). Files are size-capped before being read into memory (manifest 64 KB, imagery 20 MB; over-cap answers 413). - Live2D models are served by closure: only the manifest, the declared primary assets, and the files the
.model3.jsonreferences (each screened against traversal, absolute and URL forms). - The plugin never downloads executables and never bundles the Live2D Cubism Core.
- Manifests are fail-closed on structure: unknown fields or renderers reject the entry with a diagnostic shown in settings.
Telemetry
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See docs/telemetry.md for the full contract.







