Unity Tools Base is the shared dependency for Unity Play Tools and Unity Edit Tools. It installs one isolated Editor-only UPM package and owns the authenticated loopback bridge, capability state, optional-service registry, compatibility checks, validation, and transactional installer.
The VS Code extension runs workspace-first, with a local UI-host fallback, so filesystem operations and the 127.0.0.1 bridge stay co-located with the Unity project in supported remote workspaces.
Normal installation and update use a fast transactional structural path that may run while Unity is open. This path verifies the trusted signed payload, destination ownership, exact file hashes, and real Unity GUID claims before atomically publishing only suite-owned roots; a successful structural transaction is an installed, usable component. Untrusted workspaces, unowned destinations, modified owned files, incompatible dependencies, and unfinished or untrusted journals still block instead of being overwritten or guessed around. Project state, journals, and exact backups live under .gangdol-unity-tools-suite; no project-owned installer executable is installed.
In a trusted Unity workspace without a current receipt, Base activates after startup, discovers installed trusted Unity Play Tools and Unity Edit Tools host extensions, and best-effort reconciles Base plus every discovered child through the same per-project serialized transaction queue. Extension inventory changes trigger a debounced reconciliation, so children installed together with Base are not omitted merely because their activation order differed. Each child result is retained independently: one failure does not prevent the remaining discovered children from being attempted. An exact full receipt is written to host-global state only after the durable structural write succeeds, is never evicted by project count, and binds the canonical project plus the exact extension/build and workspace-composition identity. Non-Unity and untrusted workspaces are left untouched. True ownership, journal, dependency, or durable-state failures never return false full success and are surfaced with the affected component and reason.
Commands
- Unity Tools: Install Unity Tools Base performs the fast, idempotent structural install or update and normally does not require Unity to close.
- Unity Tools: Repair Unity Tools Installation recovers interrupted transactions, then performs the expensive Unity batch/deep validation. Repair may require Unity to be closed.
- Unity Tools: Retry All Unity Tools Installations immediately retries Base and every currently installed trusted child extension without requiring a window reload.
- Unity Tools: Remove a Unity Tools Package removes a selected package after Unity validation, then best-effort removes only its exact, unmodified project skill.
- Unity Tools: Show Unity Tools Status reports effective capability and owned-file integrity state.
Extension API v1
Extensions that declare "extensionDependencies": ["gangdol.unity-tools-base"] can activate Base and use:
const base = await vscode.extensions.getExtension('gangdol.unity-tools-base').activate();
const automaticRoot = base.resolveAutomaticUnityProjectRoot(); // safe workspace-associated root
const projectRoot = base.resolveUnityProjectRoot(resourceUri); // synchronous string | undefined
const legacyProtected = base.isLegacyProtected({ projectRoot }); // read-only coherent-footprint check
await base.ensureComponentInstalled(extensionJsonPath, { projectRoot });
const status = await base.getStatus({ projectRoot });
const descriptor = await base.readBridgeDescriptor({ projectRoot });
const descriptorWatch = base.watchBridgeDescriptor({ projectRoot }, onDescriptorChanged);
// descriptorWatch.dispose() when the consumer is disposed.
activate() returns apiVersion, extensionId, resolveAutomaticUnityProjectRoot,
resolveUnityProjectRoot, isWorkspaceTrusted, isLegacyProtected, ensureBaseInstalled,
ensureComponentInstalled, retryInstalledComponents, getStatus, repair, isCapabilityEnabled, and
readBridgeDescriptor and watchBridgeDescriptor. The descriptor watcher only
marks a bridge-file generation change. On that generation only, the runtime provider
performs one bounded direct read of the fixed project-local file and strict schema parse;
the consumer's requested token-authenticated route is the live authority. It does not
invoke installer status, inventory hashing, ACL/native verification, PID probing, or an
extra status challenge. Automatic discovery accepts a canonical Unity workspace
folder, a trusted configured Unity root contained by an open workspace, or the
Unity ancestor of the active file when that ancestor is inside an open workspace.
Resource-driven/manual discovery may likewise walk to the containing project.
Normal ensure operations use the fast transactional structural path. Deep Unity
batch validation is reserved for explicit Repair or an explicitly requested
capability revalidation.
Ensures are serialized per canonical project and duplicate component requests are coalesced. Base also reconciles all installed trusted child extensions on startup and when the VS Code extension inventory changes. Retry All Unity Tools Installations re-enters that aggregate reconciliation directly, including after a prior failed attempt, so reloading the window is unnecessary. Normal activation with an exact same-build full receipt performs no installer status, manifest parse, hash, ACL, process, or revalidation work. Ambient Unity/dependency drift does not trigger an automatic deep-validation prompt or change successful runtime behavior. Missing setup may start detached best-effort reconciliation; true conflicts remain blocked and visible instead of silently disappearing.
The normal structural install path never probes or launches Unity and therefore
needs no Unity-launch agreement. Before an explicit Repair, removal, or other deep
operation may probe or launch Unity, Base shows a modal agreement
containing the canonical project, its declared Unity version, every component and
operation in the plan, the exact installed extension source and manifest digest,
and either the resolved executable path/digest or the standard Unity-discovery plan.
On macOS, the prompt states that Unity loads only a private same-volume
copy-on-write project snapshot: existing InitializeOnLoad and AssetPostprocessor
code can run, but Unity's ordinary project and import-cache writes through the
loaded project path remain in that snapshot. The snapshot is not an OS sandbox;
Editor code can still perform external side effects allowed to the current account.
On Windows, the prompt retains the warning that Unity loads the real
project and existing Editor code can modify project files or import state as in an
ordinary Editor load. Agreement is in-memory, exact-plan-bound, single-use, and
never persisted. Dismissal or Not Now launches nothing and starts no new
mutation; a deferred retry asks again. Exact read-only status and doctor health
checks need no agreement.
Journal recovery has its own separate modal agreement because it can restore or
roll back files even though it never launches Unity. Before recovery, Base shows
the canonical project, exact .gangdol-unity-tools-suite storage, and the exact
suite-owned journal path, status, and SHA-256. The installer rechecks that binding
before loading the recovery plan, so a same-name replacement is preserved rather
than recovered. Dismissal or Not Now performs zero recovery mutation and
returns recovery-agreement-declined with state preserved. An exact in-memory
grant may be reused only for the same deferred journal identity after a Unity-lock
race; it never authorizes installation, removal, or a Unity launch.
For a future incompatible Base release, publish a child update whose dependency range overlaps both the installed and target Base versions. Base installs and validates that bridging child first, then updates Base. A non-overlapping child/Base pair is deliberately blocked so no update can strand an installed component.
All automated operations execute assets/base/installer.js with VS Code's bundled runtime (ELECTRON_RUN_AS_NODE=1), without a shell, PATH lookup, Python installation, or project-owned executable. The Python implementation remains source-only as a development oracle and is excluded from the VSIX and installed project.
The following launch isolation applies only to explicit Repair/deep validation and
other operations that actually need Unity; it is not part of the normal structural
install path. On macOS, an approved Unity source is never launched by its mutable pathname.
After agreement, Base passes its hash-pinned universal clone helper to the already
attested installer over a retained descriptor. The helper uses fclonefileat with
CLONE_NOFOLLOW_ANY | CLONE_NOOWNERCOPY to create a clone-only, same-volume
Unity.app snapshot in a random owner-private directory. Base rejects absolute,
broken, or escaping bundle symlinks, verifies the staged executable SHA-256, then
runs strict/deep code-sign validation against Unity's exact identifier and team
before launching only the staged executable. There is no source-path, copy, or
reduced-security fallback. The snapshot is kept through validation and removed
only when its ownership and post-run integrity remain exact. Cloning and code-sign
verification affect explicit deep validation or validation-requiring removal only;
normal structural installation and Play/Edit runtime paths do not use them.
The macOS batch-validation project is independently isolated. Base creates a
private same-volume copy-on-write snapshot of the project and passes only that
snapshot to Unity, so Unity never opens the real project or its real Library
cache. Existing Editor initialization and asset-processing code can run normally;
Unity's ordinary writes through the loaded snapshot path are not published. After
a successful validation, the only Unity-produced project postimage that may be
published to the real project is a bounded Packages/packages-lock.json, and only after its
transition passes the exact semantic dependency rules and the real preimage is
still unchanged. Declared local file: package dependencies outside the project
are copy-on-write cloned into the same private stage and rebound only inside the
snapshot; their exact original lock specifications are restored before validation
and publication. Unsafe, ambiguous, or cross-filesystem local-package topology
fails before Unity launches rather than exposing the external source to Editor
writes. Cleanup removes only the exactly identified private snapshot; if
safe cleanup cannot be proven, Base fails safely and preserves the snapshot for
inspection rather than widening deletion. The snapshot confines normal Unity writes
to the project path, but does not sandbox arbitrary Editor code from other files,
processes, or services accessible to the current account. This project-snapshot
isolation is macOS-specific; Windows validation behavior is unchanged.
The macOS tree preflight treats only a real root .git directory as VCS
metadata, so normal Git filesystem-monitor sockets do not block installation and
are not touched. A root .git symlink is still inspected and rejected if unsafe,
worktree-style .git files remain ordinary validated files, and special entries
outside that exact VCS directory remain installation-blocking conflicts.
Unity Multiplayer Play Mode may generate absolute project links and cached
absolute paths under Library/VP. Base leaves the real cache completely
untouched, omits only a real Library/VP directory from source-content scanning,
and removes only its copy inside the private validation project before the normal
snapshot boundary scan and Unity launch. A symlinked or otherwise unsafe
Library/VP root is rejected, active virtual-project lockfiles block mutation,
and absolute links elsewhere remain conflicts.
On Windows, Base ships a source-available, SHA-256-pinned native verifier and
passes its exact bytes to the installer over a retained descriptor. The verifier
accepts only local fixed NTFS paths, opens every ancestor and leaf without
following reparse points, validates handle-bound owner/DACL and file identity,
rejects hard links, and rechecks the same handles before and after bounded reads.
Consent and operation-lock files are consumed by their exact identity and digest,
so a raced replacement is preserved. Unity candidates require handle-bound ACL
validation and Authenticode trust; the verifier retains the validated path chain
across a shell-free launch and revalidates it afterward. These checks affect only
explicit repair/revalidation, validation-requiring removal, and Unity batch validation.
Normal Play/Edit descriptor reads and routes never invoke the verifier. After a successful
installation, Windows exposes the same Base bridge and runtime capabilities as other
supported hosts, without a reduced-performance path. Unsafe or unverifiable installer
input fails closed before Unity or project mutation. Linux Unity-validating operations
remain unavailable pending an equivalent native launch boundary.
The Windows process boundary is itself release-pinned: a bounded PowerShell
bootstrap authenticates a source-available launcher and a deterministic launcher
assembly, which retain every verifier path component and the verifier file without
delete/write sharing through process creation. The main verifier and launcher
assembly have VM-reproducible Roslyn build scripts that pin the compiler itself by
SHA-256. Release tests compile each artifact twice on local NTFS, compare the
resulting SHA-256 values to the bundled bytes, and run both ordinary and elevated
adversarial suites against the exact bundled verifier. A VM-only discovery
regression also proves that a safe candidate with the wrong version is skipped in
favor of a later exact-version candidate.
Installer and Unity child processes receive a minimal cross-platform environment:
OS/runtime paths, temporary directories, locale/time-zone, display/session, and
explicit graphics-device variables only. API keys, *_TOKEN values, proxy
credentials, CODEX_HOME, auth paths, NODE_OPTIONS, and other ambient extension-host
variables are not forwarded to project-controlled Unity Editor code.
Payload manifest v1 gives each capability independent required and requiresGraphics flags. A normal install establishes the signed package structurally: Base verifies the manifest-bound payload and exact installed inventory, records it as installed, and does not launch a second Unity process merely to repeat those checks. Explicit Repair/deep validation additionally exercises the package in Unity. That deep pass first validates with graphics, then may retry headlessly when the required core permits it. If every required capability validates but an optional capability is explicitly disabled with a reason, Base records the deep result as non-success partial; individually enabled unaffected capabilities remain usable, and no lower-performance substitute is introduced. The exact deep-validation path is persisted with the result. The Base bridge itself is required.
An exact headless-fallback installation may remain a validated partial core,
but is never reported as healthy success and never enables a reduced graphics
implementation. Once per component and
project during each VS Code extension-host session, Base may offer Retry Graphics
Validation when a graphics-preferred component was last validated without
graphics, or when an older valid state predates the validation-path field. The
validated unaffected capability set is returned immediately; activation never waits
for this offer. The offer itself does not resolve, probe, or launch Unity. Accepting it only routes the
component to the existing exact Unity agreement dialog; Unity can run only after
that second agreement. Declining either step leaves the installed last-known-good
state untouched. Here last-known-good means the exact installed payload that
completed structural installation or later Unity validation; only an
all-capabilities-enabled payload has aggregate
state enabled. A changed candidate that would disable or remove a previously
enabled capability is rolled back so the prior payload remains active. A failed
graphics attempt may validate headlessly as non-success partial.
A child may isolate its exact project skill from its Unity package with
installDomains: [{"id":"project-skill","kind":"host","root":".agents/skills/<extension-id>","validation":"structure","componentRequired":true,"minimumComponentVersion":"0.1.0","maximumComponentVersionExclusive":"0.2.0"}].
Base commits that narrowly allowlisted root alongside the normal structural package transaction and records it
separately in state.json.installDomains. With componentRequired:false, a Unity
package conflict leaves the verified host skill available while the package result
reports the failure. With componentRequired:true, a domain failure blocks
the dependent package so unusable guidance is never advertised as enabled.
Conflicting skill roots are preserved untouched. Domain and component digests are
independent, so changing one does not force replacement of the other. Fresh installs
commit the standalone skill first. Updates use the installed and target declared
compatibility ranges to choose package-first or bridge-safe skill-first order; if no
safe order exists, nothing changes. If a compatible package update succeeds but a
later required domain update conflicts, the package may remain validated but the
aggregate result is non-success partial with operation
component-enabled-domain-disabled, never a fully completed extension update.
Each child manifest and installed state must retain exactly one canonical, versioned unity-tools-base dependency. Unity's -version result must exactly match ProjectSettings/ProjectVersion.txt before an explicit deep operation starts Unity against the project. If state was lost, a complete orphaned package, skill, or Base installer artifact may be adopted only when its recursive inventory and every hash exactly match the manifest; an exact structural adoption is considered installed, while Repair remains available for deep validation.
Unity validation snapshots bounded, regular, single-link protected project files,
including Packages/manifest.json, Packages/packages-lock.json, the project
version, and declared validation fingerprints. On macOS these checks are applied
to the private project snapshot first; no real-project Unity postimage other than
the semantically accepted bounded package lock is publishable. The target package's
exact embedded lock entry and only its causally reachable, exclusively owned
dependency closure may be added, removed, or updated. A direct project dependency
or a dependency also reachable from any unrelated retained package is protected
and must remain semantically identical. If a project has no lock preimage and Unity
creates entries outside that exact closure, the operation rolls back the package,
state, and new lock rather than guessing ownership. Opening the project once
normally to let Unity establish its lock baseline is the supported preparation for
that case.
Automatic Unity discovery checks the documented Hub candidate paths in order and
uses the first executable whose exact -version matches the project. On Windows,
the Hub candidates are followed by the official direct-installer roots
C:\Program Files\Unity <version>\Editor\Unity.exe and its Program Files (x86)
equivalent; the native-architecture verifier rejects an emulated executable before
its version probe. Multiple same-version installs are therefore supported
deterministically; an explicitly configured executable remains authoritative and
must itself match exactly.
The bridge descriptor is Library/com.gangdol.unity-tools/bridge.json. Normal Unity
startup publishes it through the original same-directory temporary-file delete/move
sequence, with the original owner-only 0700/0600 chmod behavior on POSIX.
It does not run private-storage ACL verification, PID challenges, or installer policy;
shutdown directly removes only a descriptor whose token still belongs to that listener.
The durable .base-owner.json, transaction roots, leases, and strict storage validation
remain installer/Unity-batch concerns and are not consulted by successful routes.
While the listener is healthy, Base retains no EditorApplication.update
callback and runs no periodic file-hash timer. Authenticated main-thread demand is
coalesced through Unity's captured synchronization context. Requests present at
one stable queue cutoff are dispatched together without installer-state or file
integrity checks in the route path. An Editor update callback is attached only
while a failed listener retry is pending, or as a wake fallback on Editors that
do not expose a synchronization context, and is removed after success or
shutdown.
Base owns the namespaced GET /v1/base/status and
GET /v1/base/prefab-preview routes. Unity batch validation attempts that exact
runtime route pair through the real ownership-enforcing router and releases the
temporary registrations before it can enable the required bridge capability.
Child Editor packages acquire durable journal storage through
UnityToolsPrivateProjectStorage.TryAcquireExtensionDirectory; the returned lease
owns Library/com.gangdol.unity-tools/transactions/<extension-id> across processes and resolves
direct filenames only. The same API provides durably flushed atomic UTF-8 writes,
non-reparse path checks, and exact-identity regular-file reads/deletes that reject
hard links and detect replacement. Separate extensions may retain independent
leases without cleaning or blocking another extension's active journal staging.
On macOS and Linux, every Base-owned storage directory/file is owner-only
(0700/0600) and unsafe existing permissions are never repaired in place.
Windows requires the canonical project Library path and inherited, unprotected
DACLs for every Base-owned child; the owner must be the current user, and effective
allow entries are limited to that owner, LocalSystem, and built-in Administrators.
Those rules protect installer consent, locks, journals, validation artifacts, and
transaction storage. They are never a successful runtime descriptor or route gate:
the host performs the bounded direct descriptor read described above, then the actual
token-authenticated requested route proves usability. Broader or unprovable access
still fails the affected installer/validation operation safely before mutation;
once validation succeeds, enabled is platform-neutral and unrestricted.
POSIX exact deletion holds an exclusive file handle and rechecks its device/inode,
link count, size, and modification stamp before and after unlink while the Base
lease is held. This contract treats the owning OS user as trusted; POSIX has no
general compare-and-unlink primitive that can exclude a hostile same-UID process
which deliberately bypasses the owner-private directory and lease protocol.
Installer operation locks use an exact schema bound to the canonical project and
host. A same-owner lock is reclaimed only after it is at least 60 seconds old and
its PID is proven absent. POSIX holds an open descriptor and rechecks suite/path
identity through randomized quarantine and unlink. Windows uses the native
verifier's private ACL read plus exact NTFS identity-and-digest consumption; the
opened object is marked for deletion while its handles remain held, and a raced
replacement is preserved. Active, young, malformed, cross-project, replaced, or
unprovable locks are preserved on every host.
Publication status
Version 0.1.0 is licensed for public distribution through the Visual Studio Marketplace under the bundled proprietary license.
Windows live release verification
The release-only Windows suite harness is
tests/live/windowsSuiteInstallE2E.js. It is excluded from
the VSIX and from ordinary test execution. Merely importing it cannot launch Unity. Execution
requires Windows and the exact UNITY_TOOLS_WINDOWS_SUITE_LIVE_E2E_AGREEMENT value documented in
tests/live/WINDOWS_SUITE_DRIVER.md.
The harness accepts only an exact GUID-marked project produced by the Windows Play live harness on
a VM-local fixed NTFS volume. It rejects shared/network and reparse paths, requires Unity
6000.4.4f1 changeset 360f97ecca93, and pins the signed native ARM64 VS Code 1.135.0 build used
by the authorized guest. It separately pins the official adjacent bin\code.cmd and the build's
commit-specific resources\app\out\cli.js; CLI installs use only that launcher, while UI sessions
use only the signed Code.exe. It stages all VS Code user data, extensions, VSIX artifacts, driver
code, and results beneath that disposable project and sets extensions.autoUpdate to the exact
VS Code 1.135 value off while disabling update checks in the isolated profile. The harness never
installs an extension globally.
After the external CLI installs all three local VSIX files, a mandatory preactivation barrier
revalidates Code.exe, code.cmd, and cli.js, then compares the extensions' complete archive and
installed inventories, hashes, and exact isolated registry; duplicate, traversal, reparse,
hard-link, missing, or extra entries fail closed before Code opens.
The first fresh extension host exercises the production fast structural install path, including
automatic Base/Play/Edit discovery and reconciliation, and proves the explicit no-change ensures
plus fully enabled status without a Unity-launch agreement. After normal shutdown, a second fresh host
reuses the same isolated profile, activates the real Base/Play/Edit runtime surfaces, and proves
receipt-only startup initialized no installer loader, verifier bootstrap, retry, mutation, prompt,
or timer and did not change the project. It deliberately invokes no installer or status API.
Cleanup is bound to native verifier-originated Unity launch evidence and refuses changed identity,
missing GUID/checkpoint, active driver, live Unity, project mutation, or bridge evidence. The exact
two-session UI/process orchestration and its complementary no-prompt/no-Unity observation contract
are documented in the linked driver guide.