Item F (slice-1 fix round). IPluginHost.VtankProfileDirectory handed the
plugin a raw string path and told it to fall back to its own
System.IO-based portable default when null — a plugin reading and
resolving filesystem paths itself, which is exactly the seam the rest of
IPluginHost.Storage deliberately avoids (Core.Plugins.ScopedPluginHost
scopes/validates every key; the plugin never sees a path).
- IPluginHost: VtankProfileDirectory (string?) deleted; new VtankProfiles
(IPluginStorage, defaults to NoOpPluginStorage) added — a second,
UNSCOPED storage instance (unlike Storage, which Core scopes per
plugin manifest id) rooted at a host-composed VTank-compatible
directory.
- ScopedPluginHost.VtankProfiles forwards _inner.VtankProfiles directly
(no scoping — it names one shared external location, not per-plugin
data). New PluginSessionTests.ScopedHostForwardsVtankProfilesUnscoped
proves the forwarded instance is the exact same object (Assert.Same),
not a wrapper.
- AppPluginHost/Program.cs: new vtankProfiles constructor parameter,
composed as FilePluginStorage(runtimeOptions.VtankProfileDirectoryOverride
?? Path.Combine(applicationPaths.DataDirectory, "vtank")).
- RuntimeOptions.VtankProfileDirectoryOverride: new init-only property
parsed from ACDREAM_VTANK_PROFILE_DIR (row added to
docs/launch-options.md, side-effects column states the redirect is the
only effect and documents the NullIfEmpty whitespace-not-special-cased
quirk it shares with every other path-override flag). New
RuntimeOptionsTests.VtankProfileDirectoryOverrideIsNullUnlessSet.
- FilePluginStorage.List(prefix): empty prefix now means "the storage
root itself" instead of throwing (Resolve() rejects empty/whitespace
keys, which is correct for every OTHER caller but wrong for "list
everything" — VtankProfileDirectory needs exactly that).
- Headless: HeadlessPluginHost gained the same VtankProfiles
property/constructor param, threaded through HeadlessPluginSession.Create
-> HeadlessSessionHost -> HeadlessProcessHost, composed from the new
HeadlessPathSet.VtankProfilesDirectory (<DataDirectory>/vtank, no
ACDREAM_VTANK_PROFILE_DIR-equivalent override — Headless path overrides
are HeadlessPathOverrides/CLI flags, not env vars). A small
AcDream.Headless.Plugins.FilePluginStorage duplicates the App
implementation byte-for-byte (Headless does not reference AcDream.App
and no shared "platform plugins" library exists yet to host one copy;
documented as a reasonable future consolidation, not required here).
- VtankProfileDirectory.cs rewritten: Resolve/PortableDefault deleted
outright (no more System.IO, no plugin-owned portable-default fallback);
ListSettingsProfiles/ListNavigationProfiles/ListMetaProfiles now take
IPluginStorage and enumerate through EnumerateFileNames, which calls
storage.List(string.Empty) and skips any key containing '/' (VTank's
profile directory is flat; a nested key from some other IPluginStorage
implementation is not a profile file). VtankProfileDirectoryTests
rewritten against an in-memory IPluginStorage fake instead of real
temp directories; new NestedPathKeysAreNotTreatedAsProfileFiles pins
that skip. The prior Resolve/PortableDefault-specific tests (Linux-path
guarantee, host-override-vs-portable-default) are superseded by
RuntimeOptionsTests.VtankProfileDirectoryOverrideIsNullUnlessSet plus
the RuntimeOptions.FromEnvironment Path.Combine-only composition in
Program.cs.
- docs/architecture/acdream-architecture.md: one sentence in the
Storage/List(prefix) paragraph naming VtankProfiles as the second,
unscoped storage.
No production caller of VtankProfileDirectory's listing methods exists
yet (A2's foundation is not wired into MossTankProfileStore/
MossTankMetaProfileStore/MossTankRouteProfileStore's own selection —
per that slice's own ledger note), so this is a contract + plumbing
change with no MossTank runtime behavior change.
MossTank suite: 562/562. Core.Tests (Plugin filter): 50/50. App.Tests
(Plugin|LaunchOptions|RuntimeOptions filter): 135/135. Headless.Tests:
173/174 (the one failure, HeadlessCredentialResolverTests.
LinuxRejectsGroupOrOtherCredentialPermissions, is a pre-existing
Linux-only lane gate that throws PlatformNotSupportedException on this
Windows host — unrelated to this change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>