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
12 changes: 6 additions & 6 deletions .code-linter.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"max_file_lines": 750,
"max_function_lines": 150,
"max_nesting_depth": 8,
"max_parameters": 8,
"max_comment_lines": 15,
"max_types_per_file": 6,
"max_file_lines": 300,
"max_function_lines": 50,
"max_nesting_depth": 4,
"max_parameters": 5,
"max_comment_lines": 5,
"max_types_per_file": 2,
"include_extensions": [
".cs",
".py"
Expand Down
8 changes: 8 additions & 0 deletions .githooks/post-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh

Check warning on line 1 in .githooks/post-commit

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Unknown Text/Config is not mapped to a structural checker.
set -eu

REPO_ROOT=$(git rev-parse --show-toplevel)

if [ -d "$REPO_ROOT/.projectmem" ] && command -v pjm >/dev/null 2>&1; then
(cd "$REPO_ROOT" && pjm _auto-capture commit >/dev/null 2>&1 &)
fi
8 changes: 8 additions & 0 deletions .githooks/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh

Check warning on line 1 in .githooks/post-merge

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Unknown Text/Config is not mapped to a structural checker.
set -eu

REPO_ROOT=$(git rev-parse --show-toplevel)

if [ -d "$REPO_ROOT/.projectmem" ] && command -v pjm >/dev/null 2>&1; then
(cd "$REPO_ROOT" && pjm _auto-capture merge >/dev/null 2>&1 &)
fi
8 changes: 8 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh

Check warning on line 1 in .githooks/pre-commit

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Unknown Text/Config is not mapped to a structural checker.
set -eu

REPO_ROOT=$(git rev-parse --show-toplevel)

if [ -d "$REPO_ROOT/.projectmem" ] && command -v pjm >/dev/null 2>&1; then
(cd "$REPO_ROOT" && pjm precheck --level warn) || true
fi
10 changes: 10 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
* @daliys

Check warning on line 1 in .github/CODEOWNERS

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Unknown Text/Config is not mapped to a structural checker.

# Quality Gate & CI Policy files strictly owned by @daliys
/.code-linter.json @daliys
/.ruff.toml @daliys
/.unity-quality-gate.json @daliys
/.swift-quality-gate.json @daliys
/.slop-review.json @daliys
/.github/workflows/ @daliys
/.github/CODEOWNERS @daliys
69 changes: 69 additions & 0 deletions .github/workflows/approve-external-pr.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Approve external PR for CI

Check warning on line 1 in .github/workflows/approve-external-pr.yml

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Supported extension '.yml' is not included by the active policy.

# ──────────────────────────────────────────────────────────────────────────────
# Maintainer "Approve & run CI" button for external (fork) pull requests.
Expand Down Expand Up @@ -29,6 +29,11 @@
description: "External PR number to replay (e.g. 30)"
required: true
type: string
acknowledge_critical_files:
description: "Set true only after reviewing the diff, to proceed even though the PR touches .github/workflows, .githooks, or scripts"
required: false
type: boolean
default: false

permissions:
contents: write # push the trusted/pr-N branch
Expand Down Expand Up @@ -79,6 +84,70 @@
exit 1
fi

# ── 0.1. Authorize maintainer actor ─────────────────────────────────────
- name: Verify maintainer authorization
env:
ACTOR: ${{ github.actor }}
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

echo "Verifying authorization for actor @$ACTOR..."

# Explicit maintainer bypass
if [ "$ACTOR" = "Daliys" ]; then
echo "Actor @$ACTOR is authorized maintainer."
exit 0
fi

# Check collaborator permission via GitHub API
PERM=$(gh api "repos/$REPOSITORY/collaborators/$ACTOR/permission" --jq '.permission' 2>/dev/null || echo "none")
case "$PERM" in
admin|write)
echo "Actor @$ACTOR authorized with permission: $PERM"
;;
*)
echo "::error::Actor @$ACTOR does not have maintainer authorization (permission: $PERM)."
exit 1
;;
esac

# ── 0.2. Content & Security Pre-scan ────────────────────────────────────
- name: Run security pre-scan on PR files
env:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
ACKNOWLEDGE: ${{ inputs.acknowledge_critical_files }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

echo "Scanning changed files in PR #$PR_NUMBER..."
FILES=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')

SUSPICIOUS_COUNT=0
while IFS= read -r file; do
[ -z "$file" ] && continue
case "$file" in
.github/workflows/*|.githooks/*|scripts/*)
echo "::warning::PR #$PR_NUMBER modifies critical CI/script file: $file"
SUSPICIOUS_COUNT=$((SUSPICIOUS_COUNT + 1))
;;
esac
done <<< "$FILES"

if [ "$SUSPICIOUS_COUNT" -gt 0 ]; then
echo "::notice::PR #$PR_NUMBER modifies $SUSPICIOUS_COUNT critical file(s)."
if [ "$ACKNOWLEDGE" != "true" ]; then
echo "::error::Refusing to replay PR #$PR_NUMBER: it modifies critical CI/script file(s) (see warnings above). Review the diff, then re-run this workflow with 'acknowledge_critical_files' set to true if it's safe to proceed."
exit 1
fi
echo "Critical file changes acknowledged by @${{ github.actor }} — proceeding."
else
echo "Content pre-scan passed cleanly: no critical CI/workflow files modified."
fi

# ── 1. Create trusted branch with the fork's commits ───────────────────
- name: Create trusted/pr-${{ inputs.pr_number }} branch
env:
Expand Down
21 changes: 14 additions & 7 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,21 @@ jobs:
path: candidate

- name: Run required Ollama documentation and checklist review
env:
IS_FORK_PR: ${{ github.event.pull_request && github.event.pull_request.head.repo.fork || false }}
run: |
set -euo pipefail
dotnet --version

QUALITY_GATE_PROJECT="trusted/tools~/NexusQualityGate/NexusQualityGate.csproj"
if [ ! -f "$QUALITY_GATE_PROJECT" ]; then
echo "::notice::Trusted branch does not have NexusQualityGate yet; using candidate tool for bootstrap validation."
QUALITY_GATE_PROJECT="candidate/tools~/NexusQualityGate/NexusQualityGate.csproj"
elif ! grep -q -- "--checklist-ai" trusted/tools~/NexusQualityGate/QualityGateOptions.cs; then
echo "::notice::Trusted NexusQualityGate does not support checklist AI yet; using candidate tool for this bootstrap validation."
QUALITY_GATE_PROJECT="candidate/tools~/NexusQualityGate/NexusQualityGate.csproj"
if [ ! -f "$QUALITY_GATE_PROJECT" ] || ! grep -q -- "--checklist-ai" trusted/tools~/NexusQualityGate/QualityGateOptions.cs; then
if [ "${IS_FORK_PR:-false}" = "true" ]; then
echo "::warning::Trusted branch does not support the required NexusQualityGate version, and candidate tool cannot be executed for fork PRs due to security restrictions. Skipping AI documentation check."
exit 0
else
echo "::notice::Trusted branch does not have the required NexusQualityGate version; using candidate tool for bootstrap validation."
QUALITY_GATE_PROJECT="candidate/tools~/NexusQualityGate/NexusQualityGate.csproj"
fi
fi

dotnet run --project "$QUALITY_GATE_PROJECT" -- \
Expand Down Expand Up @@ -239,6 +243,9 @@ jobs:
dependencies = manifest.setdefault("dependencies", {})
dependencies["com.forkhorizon.nexus.unity"] = f"file:{os.environ['GITHUB_WORKSPACE']}"
dependencies["com.unity.test-framework"] = "1.5.1"
testables = manifest.setdefault("testables", [])
if "com.forkhorizon.nexus.unity" not in testables:
testables.append("com.forkhorizon.nexus.unity")
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
PY

Expand Down Expand Up @@ -361,7 +368,7 @@ jobs:
-projectPath "$SMOKE_ROOT" \
-runTests \
-testPlatform editmode \
-assemblyNames NexusUnity.PackageSmoke.EditorTests \
-assemblyNames "NexusUnity.PackageSmoke.EditorTests;UnityMCP.Editor.Tests" \
-testResults "$SMOKE_TEST_RESULTS" \
-logFile "$SMOKE_TEST_LOG"; then
echo "::error::Unity package Editor tests failed."
Expand Down
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ PYTHONDONTWRITEBYTECODE=1 scripts/prepush-validate.sh --static-only
For Unity-facing behavior, also use the running test harness through MCP tools such as `unity_wait`, `unity_lint_project`, and `unity_editor_controller`.

Clean generated Python caches before validation if needed; `__pycache__` or `*.pyc` files are not part of package changes.

<!-- SOMA_MEMORY_TOOLS_START -->
## Memory Tools

Default mode: light. Do not spend tokens on memory tools for small, obvious, single-file tasks.

- projectmem: use for bugs, regressions, multi-step changes, repeated attempts, or architecture decisions. For small self-contained edits, skip full memory startup and use targeted history checks only when useful.
- `.projectmem/config.toml`, `PROJECT_MAP.md`, and `plan.md` are shared project configuration. The append-only event log, issue files, generated summary/structure, watcher state, and generated `CLAUDE.md` bridge are local and ignored.
- This package uses the tracked `.githooks` directory. When `pjm` is installed, its `pre-commit`, `post-commit`, and `post-merge` wrappers provide warnings and local auto-capture; do not run `pjm hooks install`, which assumes `.git/hooks` exists.
- Verify that the projectmem MCP server is bound to this package before using write tools. If it is bound elsewhere, use the local `pjm` CLI from this package root instead.
<!-- SOMA_MEMORY_TOOLS_END -->
9 changes: 6 additions & 3 deletions API_REFERENCE.MD
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Nexus Unity API Reference

Version: `1.5.0`
Version: `1.6.0`

Nexus Unity exposes two supported public API surfaces:

Expand Down Expand Up @@ -187,12 +187,12 @@ Actions: `search`, `explore`, `create_material`, `import`, `refresh`, `instantia
### `unity_editor_controller`
Actions: `undo`, `redo`, `play`, `pause`, `step`, `menu`, `read_logs`, `clear_logs`, `get_state`, `get_server_status`, `refresh_assets`, `run_tests`, `get_test_results`, `run_tests_wait`, `get_tool_usage_stats`, `reset_tool_usage_stats`.

`run_tests_wait` triggers `run_tests` and polls raw `get_test_results` from the Python bridge so Unity's main thread is not blocked while the agent waits.
`get_tool_usage_stats` returns in-memory call counts, average/total durations, `last_error_type`, and sanitized `last_error` with sensitive paths redacted; `reset_tool_usage_stats` resets the counters. Raw `run_tests` returns `status: "Submitted"` after Unity accepts the asynchronous request; it does not claim that execution has begun or completed. `run_tests_wait` triggers `run_tests` and polls raw `get_test_results` from the Python bridge so Unity's main thread is not blocked while the agent waits. If submission is rejected, `run_tests_wait` returns that result without polling.

### `unity_ui_automation`
Actions: `list_windows`, `get_hierarchy`, `query`, `get_window_rect`, `set_window_rect`, `capture_window_snapshot`, `click`, `input`.

`get_hierarchy` accepts `deep`. `query` accepts `name`, `text`, and `class_name`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy, with best-effort PNG image capture on macOS.
`get_hierarchy` accepts `deep`, `max_depth`, and `max_elements` (with `children_truncated` and root `truncated` markers when capped). `query` accepts `name`, `text`, `class_name`, `max_depth`, and `max_results`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy (supporting `max_depth` and `max_elements`), with best-effort PNG image capture on macOS.

### `unity_wait`
Conditions: `compilation`, `play_mode`, `import`, `editor_idle`.
Expand All @@ -209,6 +209,9 @@ This is the public MCP bridge macro for code edits. Pass `confirm: true` when wr

Raw `attach_script`, `write_file`, and `write_files_batch` calls also require `confirm: true` before writing `.cs` files.

### `delete_asset`
Deletes a file or folder in the project. Requires `path` and `confirm: true`. Deletions use `AssetDatabase.MoveAssetToTrash` to send items to the OS Trash. Deletion of `ProjectSettings/` or `Packages/` paths is forbidden.

### `unity_invoke_method`
Invokes a C# method on a component through reflection.

Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable public changes to Nexus Unity are documented here.

## [Unreleased]

## [1.6.0] - 2026-08-23

### Security
- Sanitize and redact sensitive exception messages in tool usage stats (`get_tool_usage_stats` and `RecordToolUsage`), replacing absolute filesystem paths, user directories, and multiline frames with generic placeholders and safe truncated summaries, and exposing `last_error_type` (#142).
- Require explicit confirmation (`confirm: true`) for `delete_asset`, enforce `AssetDatabase.MoveAssetToTrash` for OS trash recovery, and block creation, modification, moving, or deletion of `ProjectSettings/` and `Packages/` paths across all asset tools (#140).
- Hardened external PR replay workflow (`approve-external-pr.yml`) with an in-workflow actor authorization gate and a PR content security pre-scan to prevent unauthorized execution and flag critical CI modifications on self-hosted runners (#144).
- The content pre-scan now blocks the replay when a PR touches `.github/workflows/*`, `.githooks/*`, or `scripts/*`, requiring the maintainer to re-run with `acknowledge_critical_files: true` after reviewing the diff, instead of only logging a warning (#144).

### Changed
- Consolidated duplicate internal Ollama-review and serialized-property write helpers.

### Fixed
- Add traversal depth and element/result bounds to UI Toolkit hierarchy serialization and element queries (`ui_get_hierarchy`, `ui_query_elements`, and `ui_capture_window_snapshot`), preventing editor stalls and payload bloat on complex UI windows, with `children_truncated` and `truncated` markers on partial hierarchy responses (#126).
- Dispatch WebSocket request handling asynchronously in `Task.Run`, ensuring `ServerLoop` remains non-blocking for concurrent HTTP JSON-RPC requests and new connections (#129).
- `EditorApplication.update` subscription for `HandlePostCompileFocusReturn` is now idempotent (unsubscribes before subscribing), preventing stacked duplicate frame handlers on repeated initialization or domain reloads (#138).
- `ListPlayerPrefs` now uses `ProcessStartInfo.ArgumentList` and timeout guards for macOS `defaults read` execution, preventing process hangs, alongside regex unhashing for Windows registry keys, Linux XML prefs support, and double-default type verification (#143).
- Persist authentication tokens across Unity Editor process restarts in Library token file, preventing persistent HTTP 401 Unauthorized errors for external MCP CLI integrations after restarting Unity (#166).
- `get_scene_dependencies` now uses `enterChildren = false` after the initial property iteration step, preventing deep recursive traversal into child properties and eliminating duplicated dependency references (#71).
- `find_objects` now safely constructs name search regexes with a 100ms match timeout and falls back gracefully to literal substring search on invalid regex patterns or match timeouts (#119).
- Restrict type resolution (`FindType`) to user project assemblies and standard public `UnityEngine` assemblies, disallow internal system/editor assemblies and namespaces, and enforce strict type allowlist constraints for component and ScriptableObject creation/inspection tools (#139).
- Resolve symlinks and directory junctions to their real filesystem targets in `ValidatePath` before checking project boundaries to prevent path traversal.
- Reject abstract ScriptableObject types before calling `CreateInstance` in `create_scriptable_object_asset` and `list_fields_for_type`.
- `TriggerSafeAssetRefresh` callback is now tracked to prevent callback leaks, duplicate refresh hooks, and memory leaks across assembly reload/shutdown boundaries, with a fail-safe 15-second timeout guard.
- `run_tests` now reports `Submitted` rather than `Success` after Unity accepts its asynchronous request, and `run_tests_wait` now returns rejected submissions immediately instead of polling until timeout.
- Auto Setup now reports a clear refresh-and-retry message if a client is unavailable after the integration list is regenerated, instead of throwing an exception.

## [1.5.0] - 2026-07-12

### Changed
Expand Down
Loading
Loading