Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions .github/workflows/unity-unterm-vendor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
name: Unity-Unterm Vendor

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
attestations: read

concurrency:
group: unity-unterm-vendor-${{ github.ref }}
cancel-in-progress: true

jobs:
vendor-integrity:
runs-on: ubuntu-latest
defaults:
run:
working-directory: unity-cursor-toolkit
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 26.4.0
cache: npm
cache-dependency-path: unity-cursor-toolkit/package-lock.json

- name: Verify age-gated npm
run: |
node -e "const version=process.version.slice(1); if(version!=='26.4.0') throw new Error('Unexpected Node version: '+version)"
node -e "const version=require('child_process').execFileSync('npm',['--version'],{encoding:'utf8'}).trim().split('.').map(Number); if(version[0]<11 || (version[0]===11 && version[1]<14)) throw new Error('npm 11.14.1 or newer is required')"
test "$(npm config get min-release-age)" = "7"

- run: npm ci
- run: npm run validate

- name: Verify fork artifact attestations
env:
GH_TOKEN: ${{ github.token }}
run: |
source_commit="$(node -p "require('../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/VENDOR.json').sourceCommit")"
source_ref="$(node -p "require('../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/VENDOR.json').sourceRef")"
verify_attestation() {
gh attestation verify "$1" \
--repo rankupgames/Unity-Unterm \
--signer-workflow rankupgames/Unity-Unterm/.github/workflows/split-upm.yml \
--source-digest "$source_commit" \
--source-ref "$source_ref" \
--deny-self-hosted-runners
}
verify_attestation ../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/Plugins/macOS/unterm.dylib
verify_attestation ../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/Plugins/macOS/unterm-debugger
verify_attestation ../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/Plugins/Windows/x86_64/unterm.dll
verify_attestation ../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/Plugins/Windows/x86_64/unterm-debugger.exe
verify_attestation "../Packages/com.rankupgames.unity-cursor-toolkit/Editor/ThirdParty/Unity-Unterm/Third Party Notices.md"

unity-6000-macos-smoke:
if: github.event_name == 'workflow_dispatch'
needs: vendor-integrity
runs-on: macos-15
environment: unity-ci
env:
UNITY_CURSOR_TOOLKIT_UNITY_PATH: /Applications/Unity/Unity.app/Contents/MacOS/Unity
defaults:
run:
working-directory: unity-cursor-toolkit
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 26.4.0
cache: npm
cache-dependency-path: unity-cursor-toolkit/package-lock.json

- name: Require protected Unity license
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: test -n "$UNITY_LICENSE_B64"

- name: Verify age-gated npm
run: |
node -e "const version=process.version.slice(1); if(version!=='26.4.0') throw new Error('Unexpected Node version: '+version)"
node -e "const version=require('child_process').execFileSync('npm',['--version'],{encoding:'utf8'}).trim().split('.').map(Number); if(version[0]<11 || (version[0]===11 && version[1]<14)) throw new Error('npm 11.14.1 or newer is required')"
test "$(npm config get min-release-age)" = "7"

- run: npm ci
- run: npm run compile

- name: Install pinned Unity Editor
env:
UNITY_INSTALLER_SHA256: ${{ vars.UNITY_6000_3_9F1_MAC_SHA256 }}
UNITY_INSTALLER_URL: https://download.unity3d.com/download_unity/7a9955a4f2fa/MacEditorInstaller/Unity-6000.3.9f1.pkg
run: |
test "${#UNITY_INSTALLER_SHA256}" -eq 64
curl --fail --location --retry 3 --output "$RUNNER_TEMP/Unity.pkg" "$UNITY_INSTALLER_URL"
printf '%s %s\n' "$UNITY_INSTALLER_SHA256" "$RUNNER_TEMP/Unity.pkg" | shasum -a 256 --check
pkgutil --check-signature "$RUNNER_TEMP/Unity.pkg" | grep -q "Status: signed"
pkgutil --check-signature "$RUNNER_TEMP/Unity.pkg" | grep -q "Developer ID Installer: Unity"
sudo installer -pkg "$RUNNER_TEMP/Unity.pkg" -target /
test -x "$UNITY_CURSOR_TOOLKIT_UNITY_PATH"

- name: Import protected Unity license
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: |
printf '%s' "$UNITY_LICENSE_B64" | openssl base64 -d -A -out "$RUNNER_TEMP/Unity.ulf"
chmod 600 "$RUNNER_TEMP/Unity.ulf"
"$UNITY_CURSOR_TOOLKIT_UNITY_PATH" -quit -batchmode -nographics -manualLicenseFile "$RUNNER_TEMP/Unity.ulf" -logFile "$RUNNER_TEMP/unity-license.log"

- name: Run Unity 6000.3 internal smoke
run: npm run test:unity-internal

- name: Remove license material
if: always()
run: rm -f "$RUNNER_TEMP/Unity.ulf" "$RUNNER_TEMP/unity-license.log"

unity-6000-windows-smoke:
if: github.event_name == 'workflow_dispatch'
needs: vendor-integrity
runs-on: windows-latest
environment: unity-ci
env:
UNITY_CURSOR_TOOLKIT_UNITY_PATH: C:\Unity\Editor\Unity.exe
defaults:
run:
working-directory: unity-cursor-toolkit
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 26.4.0
cache: npm
cache-dependency-path: unity-cursor-toolkit/package-lock.json

- name: Require protected Unity license
shell: pwsh
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: if ([string]::IsNullOrWhiteSpace($env:UNITY_LICENSE_B64)) { throw 'UNITY_LICENSE_B64 is required' }

- name: Verify age-gated npm
shell: pwsh
run: |
if ((node --version).Trim() -ne 'v26.4.0') { throw 'Unexpected Node version' }
if ([Version](npm --version) -lt [Version]'11.14.1') { throw 'npm 11.14.1 or newer is required' }
if ((npm config get min-release-age).Trim() -ne '7') { throw 'npm min-release-age=7 is required' }

- run: npm ci
- run: npm run compile

- name: Install pinned Unity Editor
shell: pwsh
env:
UNITY_INSTALLER_SHA256: ${{ vars.UNITY_6000_3_9F1_WINDOWS_SHA256 }}
UNITY_INSTALLER_URL: https://download.unity3d.com/download_unity/7a9955a4f2fa/Windows64EditorInstaller/UnitySetup64-6000.3.9f1.exe
run: |
if ($env:UNITY_INSTALLER_SHA256 -notmatch '^[a-fA-F0-9]{64}$') { throw 'UNITY_INSTALLER_SHA256 is required' }
$installerPath = Join-Path $env:RUNNER_TEMP 'UnitySetup64.exe'
Invoke-WebRequest -Uri $env:UNITY_INSTALLER_URL -OutFile $installerPath
$actualHash = (Get-FileHash -Algorithm SHA256 $installerPath).Hash.ToLowerInvariant()
if ($actualHash -ne $env:UNITY_INSTALLER_SHA256.ToLowerInvariant()) { throw 'Unity installer SHA-256 mismatch' }
$signature = Get-AuthenticodeSignature $installerPath
if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'Unity') { throw 'Unity installer signature is invalid' }
$installer = Start-Process -FilePath $installerPath -ArgumentList '/S','/D=C:\Unity' -PassThru -Wait
if ($installer.ExitCode -ne 0) { throw "Unity installer failed with exit code $($installer.ExitCode)" }
if ((Test-Path $env:UNITY_CURSOR_TOOLKIT_UNITY_PATH -PathType Leaf) -eq $false) { throw 'Unity executable was not installed' }

- name: Import protected Unity license
shell: pwsh
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: |
$licensePath = Join-Path $env:RUNNER_TEMP 'Unity.ulf'
$logPath = Join-Path $env:RUNNER_TEMP 'unity-license.log'
[IO.File]::WriteAllBytes($licensePath, [Convert]::FromBase64String($env:UNITY_LICENSE_B64))
$licenseProcess = Start-Process -FilePath $env:UNITY_CURSOR_TOOLKIT_UNITY_PATH -ArgumentList '-quit','-batchmode','-nographics','-manualLicenseFile',$licensePath,'-logFile',$logPath -PassThru -Wait
if ($licenseProcess.ExitCode -ne 0) { throw "Unity license import failed with exit code $($licenseProcess.ExitCode)" }

- name: Run Unity 6000.3 internal smoke
run: npm run test:unity-internal

- name: Remove license material
if: always()
shell: pwsh
run: Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $env:RUNNER_TEMP 'Unity.ulf'), (Join-Path $env:RUNNER_TEMP 'unity-license.log')

unity-2019-windows-exclusion:
if: github.event_name == 'workflow_dispatch'
needs: vendor-integrity
runs-on: windows-latest
environment: unity-ci
env:
UNITY_LEGACY_PATH: C:\Unity2019\Editor\Unity.exe
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Require protected Unity license
shell: pwsh
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: if ([string]::IsNullOrWhiteSpace($env:UNITY_LICENSE_B64)) { throw 'UNITY_LICENSE_B64 is required' }

- name: Install pinned Unity Editor
shell: pwsh
env:
UNITY_INSTALLER_SHA256: ${{ vars.UNITY_2019_4_40F1_WINDOWS_SHA256 }}
UNITY_INSTALLER_URL: https://download.unity3d.com/download_unity/ffc62b691db5/Windows64EditorInstaller/UnitySetup64-2019.4.40f1.exe
run: |
if ($env:UNITY_INSTALLER_SHA256 -notmatch '^[a-fA-F0-9]{64}$') { throw 'UNITY_INSTALLER_SHA256 is required' }
$installerPath = Join-Path $env:RUNNER_TEMP 'UnitySetup64-2019.4.40f1.exe'
Invoke-WebRequest -Uri $env:UNITY_INSTALLER_URL -OutFile $installerPath
$actualHash = (Get-FileHash -Algorithm SHA256 $installerPath).Hash.ToLowerInvariant()
if ($actualHash -ne $env:UNITY_INSTALLER_SHA256.ToLowerInvariant()) { throw 'Unity installer SHA-256 mismatch' }
$signature = Get-AuthenticodeSignature $installerPath
if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'Unity') { throw 'Unity installer signature is invalid' }
$installer = Start-Process -FilePath $installerPath -ArgumentList '/S','/D=C:\Unity2019' -PassThru -Wait
if ($installer.ExitCode -ne 0) { throw "Unity installer failed with exit code $($installer.ExitCode)" }
if ((Test-Path $env:UNITY_LEGACY_PATH -PathType Leaf) -eq $false) { throw 'Unity executable was not installed' }

- name: Import protected Unity license
shell: pwsh
env:
UNITY_LICENSE_B64: ${{ secrets.UNITY_LICENSE_B64 }}
run: |
$licensePath = Join-Path $env:RUNNER_TEMP 'Unity.ulf'
$logPath = Join-Path $env:RUNNER_TEMP 'unity-license.log'
[IO.File]::WriteAllBytes($licensePath, [Convert]::FromBase64String($env:UNITY_LICENSE_B64))
$licenseProcess = Start-Process -FilePath $env:UNITY_LEGACY_PATH -ArgumentList '-quit','-batchmode','-nographics','-manualLicenseFile',$licensePath,'-logFile',$logPath -PassThru -Wait
if ($licenseProcess.ExitCode -ne 0) { throw "Unity license import failed with exit code $($licenseProcess.ExitCode)" }

- name: Create Unity 2019 package compatibility project
shell: pwsh
run: |
$projectPath = Join-Path $env:RUNNER_TEMP 'unity-2019-vendor-exclusion'
$assetsPath = Join-Path $projectPath 'Assets\Editor'
$packagesPath = Join-Path $projectPath 'Packages'
$embeddedPackagePath = Join-Path $packagesPath 'com.rankupgames.unity-cursor-toolkit'
New-Item -ItemType Directory -Force $assetsPath, $embeddedPackagePath, (Join-Path $projectPath 'ProjectSettings') | Out-Null
Copy-Item -Recurse -Force (Join-Path $env:GITHUB_WORKSPACE 'Packages\com.rankupgames.unity-cursor-toolkit\*') $embeddedPackagePath
'{"dependencies":{"com.rankupgames.unity-cursor-toolkit":"file:com.rankupgames.unity-cursor-toolkit"}}' | Set-Content -Encoding utf8 (Join-Path $packagesPath 'manifest.json')
"m_EditorVersion: 2019.4.40f1`nm_EditorVersionWithRevision: 2019.4.40f1 (ffc62b691db5)" | Set-Content -Encoding utf8 (Join-Path $projectPath 'ProjectSettings\ProjectVersion.txt')
@'
using System;
using UnityEditor;
using UnityEditor.Compilation;

public static class LegacyVendorExclusionSmoke
{
public static void Run()
{
bool toolkitFound = false;
bool vendorFound = false;
Assembly[] assemblies = CompilationPipeline.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
toolkitFound |= assembly.name == "UnityCursorToolkit.Editor";
vendorFound |= assembly.name == "UnityCursorToolkit.Vendor.Unterm.Editor";
}

if (toolkitFound == false)
{
throw new InvalidOperationException("Unity Cursor Toolkit Editor assembly did not compile.");
}

if (vendorFound)
{
throw new InvalidOperationException("Unity-Unterm vendor assembly must be excluded before Unity 6000.3.");
}

EditorApplication.Exit(0);
}
}
'@ | Set-Content -Encoding utf8 (Join-Path $assetsPath 'LegacyVendorExclusionSmoke.cs')
"UNITY_LEGACY_SMOKE_PROJECT=$projectPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Prove Unity 2019.4 vendor exclusion
shell: pwsh
run: |
$logPath = Join-Path $env:RUNNER_TEMP 'unity-2019-smoke.log'
$smokeProcess = Start-Process -FilePath $env:UNITY_LEGACY_PATH -ArgumentList '-quit','-batchmode','-nographics','-projectPath',$env:UNITY_LEGACY_SMOKE_PROJECT,'-executeMethod','LegacyVendorExclusionSmoke.Run','-logFile',$logPath -PassThru -Wait
if ($smokeProcess.ExitCode -ne 0) { throw "Unity 2019.4 compatibility smoke failed with exit code $($smokeProcess.ExitCode)" }

- name: Remove license material
if: always()
shell: pwsh
run: Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $env:RUNNER_TEMP 'Unity.ulf'), (Join-Path $env:RUNNER_TEMP 'unity-license.log')
53 changes: 53 additions & 0 deletions CursorUnityTool/Assets/Editor/UnityCursorToolkitInternalSmoke.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
Expand Down Expand Up @@ -39,6 +40,7 @@ static UnityCursorToolkitInternalSmoke()

public static void Run()
{
ValidateUntermIntegration();
SessionState.SetBool(RunningKey, true);
SessionState.SetString(PhaseKey, "enterPlay");
SessionState.SetString(ResultPathKey, GetArg("-uctSmokeResultPath", "/tmp/uct-internal-smoke-result.json"));
Expand All @@ -50,6 +52,57 @@ public static void Run()
HookUpdate();
}

/// <summary>
/// Proves that the vendored Unterm assembly and toolkit menu aliases compiled without opening an Editor window.
/// </summary>
private static void ValidateUntermIntegration()
{
Type menuItemsType = Type.GetType("Unterm.Editor.ToolkitMenuItems, UnityCursorToolkit.Vendor.Unterm.Editor", false);
if (menuItemsType == null)
{
throw new InvalidOperationException("Vendored Unity-Unterm menu assembly was not loaded.");
}

string[] expectedMenuPaths =
{
"Tools/Unity Cursor Toolkit/Unterm/New Terminal",
"Tools/Unity Cursor Toolkit/Unterm/Claude Code",
"Tools/Unity Cursor Toolkit/Unterm/Code Editor",
"Tools/Unity Cursor Toolkit/Unterm/Settings"
};
PropertyInfo menuItemProperty = typeof(MenuItem).GetProperty("menuItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo menuItemField = typeof(MenuItem).GetField("menuItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (menuItemProperty == null && menuItemField == null)
{
throw new InvalidOperationException("Unity MenuItem metadata is unavailable for integration validation.");
}

HashSet<string> registeredMenuPaths = new HashSet<string>();
MethodInfo[] menuMethods = menuItemsType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic);
foreach (MethodInfo menuMethod in menuMethods)
{
object[] menuAttributes = menuMethod.GetCustomAttributes(typeof(MenuItem), false);
foreach (object menuAttribute in menuAttributes)
{
string menuPath = menuItemProperty != null
? menuItemProperty.GetValue(menuAttribute) as string
: menuItemField.GetValue(menuAttribute) as string;
if (!string.IsNullOrEmpty(menuPath))
{
registeredMenuPaths.Add(menuPath);
}
}
}

foreach (string expectedMenuPath in expectedMenuPaths)
{
if (!registeredMenuPaths.Contains(expectedMenuPath))
{
throw new InvalidOperationException("Missing Unity-Unterm toolkit menu alias: " + expectedMenuPath);
}
}
}

public static void StartViewportStream()
{
SessionState.SetBool(RunningKey, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Added `BatchCommandEntry` for `game_command` editor batchmode list/run support.
- Added command catalog capability metadata for the editor batchmode host.
- Added the `editor_validation` MCP tool and menu command for project-file regeneration plus script compile requests.
- Added the audited Unity-Unterm fork as a toolkit-internal terminal, Claude Code, code editor, completion, and debugger feature for Unity 6000.3 on macOS and Windows.
- Added Unity-Unterm launch aliases under `Tools > Unity Cursor Toolkit > Unterm`.

### Security

- Unity-Unterm MCP access is disabled by default and uses local, uncommitted current-project policies; arbitrary C# requires a separate full-machine-access opt-in, and unclassified tools never auto-run.
- Removed Unity-Unterm's Claude Code downloader; the optional panel now discovers an existing user-managed `claude` executable without installing or updating it.
- Vendored source, managed assemblies, and native plugins are pinned to an attested fork commit and verified by SHA-256.

### Fixed

- Declared the built-in JSON serialization module required by runtime game-command argument parsing.
- Added immediate TCP `ping`/`pong` handling so the extension can verify it attached to a current Unity Cursor Toolkit package.
- Restored Unity-Unterm's terminal tab icon across editor reloads and live title changes, and reduced idle texture, resize, file, and diff polling work.

## [1.1.0] - 2026-05-28

### Added
Expand Down
Loading