nexus-coding-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,118 @@
1
+ """Render safe RuntimeEvents for a terminal consumer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from nexus.domain.runtime_events import (
8
+ AgentStepCompleted,
9
+ ApprovalRequested,
10
+ ApprovalResolved,
11
+ ApprovalSubject,
12
+ ChangedFileRecorded,
13
+ ContextBuilt,
14
+ ErrorOccurred,
15
+ FinalResult,
16
+ ModelCallFinished,
17
+ ObservabilityWarning,
18
+ PlanCreated,
19
+ RepairStarted,
20
+ ReplanOccurred,
21
+ RepositoryExplored,
22
+ RunInterrupted,
23
+ RuntimeEvent,
24
+ TaskStarted,
25
+ ToolFinished,
26
+ ToolStarted,
27
+ ValidationFinished,
28
+ ValidationStarted,
29
+ )
30
+
31
+
32
+ def render_event(event: RuntimeEvent) -> bool:
33
+ """Render one event and return whether it represents success/continuation."""
34
+
35
+ if isinstance(event, TaskStarted):
36
+ typer.echo("Task started")
37
+ return True
38
+ if isinstance(event, RepositoryExplored):
39
+ typer.echo(
40
+ f"Repository explored ({len(event.relevant_paths)} relevant paths; "
41
+ f"{event.exploration_tool_calls} tool calls)"
42
+ )
43
+ return True
44
+ if isinstance(event, ContextBuilt) and event.semantic_retrieval_status:
45
+ code = event.semantic_retrieval_status
46
+ remediation = ("Run nexus index --rebuild." if code == "INDEX_INCOMPATIBLE"
47
+ else "Run nexus index." if code == "INDEX_NOT_FOUND"
48
+ else "Check embedding/database availability and retry.")
49
+ typer.echo(f"Context warning [{code}]: using lexical context. {remediation}", err=True)
50
+ return True
51
+ if isinstance(event, FinalResult):
52
+ typer.echo(event.content)
53
+ if event.diff:
54
+ typer.echo(event.diff)
55
+ return event.status.value == "COMPLETED"
56
+ if isinstance(event, PlanCreated):
57
+ typer.echo(f"Plan {event.plan_id} v{event.plan_version}")
58
+ for summary in event.step_summaries:
59
+ typer.echo(f" {summary}")
60
+ return True
61
+ if isinstance(event, ApprovalRequested) and event.subject is ApprovalSubject.PLAN:
62
+ typer.echo("Plan approval required")
63
+ typer.echo(f" scope digest: {event.resource_or_command_summary}")
64
+ return True
65
+ if isinstance(event, ApprovalResolved):
66
+ typer.echo(f"Approval {event.decision.value.lower()} ({event.subject.value.lower()})")
67
+ return True
68
+ if isinstance(event, AgentStepCompleted):
69
+ typer.echo(
70
+ f"Agent step {event.step_count} completed ({event.decision_kind.value.lower()})"
71
+ )
72
+ return True
73
+ if isinstance(event, ModelCallFinished):
74
+ usage = event.usage.availability.value.lower()
75
+ typer.echo(
76
+ f"Model {event.phase.value.lower()} finished in {event.duration_ms} ms "
77
+ f"(reported tokens: {usage})"
78
+ )
79
+ return True
80
+ if isinstance(event, ToolStarted):
81
+ typer.echo(f"Tool {event.tool_name} started")
82
+ return True
83
+ if isinstance(event, ToolFinished):
84
+ outcome = "succeeded" if event.success else f"failed [{event.error_code}]"
85
+ typer.echo(f"Tool {event.tool_name} {outcome} in {event.duration_ms} ms")
86
+ return True
87
+ if isinstance(event, ReplanOccurred):
88
+ typer.echo(f"Plan regenerated (replan {event.replan_count})")
89
+ return True
90
+ if isinstance(event, ValidationStarted):
91
+ typer.echo(f"Validation started ({len(event.check_ids)} checks)")
92
+ return True
93
+ if isinstance(event, ValidationFinished):
94
+ typer.echo(
95
+ f"Validation {event.validation_status.value.lower()} "
96
+ f"({event.duration_ms if event.duration_ms is not None else 'unobserved'} ms)"
97
+ )
98
+ return True
99
+ if isinstance(event, RepairStarted):
100
+ typer.echo(f"Repair attempt {event.repair_count} started")
101
+ return True
102
+ if isinstance(event, ChangedFileRecorded):
103
+ typer.echo(f"Changed file: {event.relative_path}")
104
+ return True
105
+ if isinstance(event, ObservabilityWarning):
106
+ typer.echo(
107
+ f"Observability warning [{event.code}]: {event.sink.value.lower()} "
108
+ f"{event.operation.value.lower()} failed; fallback={event.fallback.value.lower()}",
109
+ err=True,
110
+ )
111
+ return True
112
+ if isinstance(event, RunInterrupted):
113
+ typer.echo(event.message)
114
+ return True
115
+ if isinstance(event, ErrorOccurred):
116
+ typer.echo(f"Error [{event.code}]: {event.message}", err=True)
117
+ return False
118
+ return True
@@ -0,0 +1,14 @@
1
+ """Centralized Day 3 security policies."""
2
+
3
+ from nexus.security.approval_policies import AutoApprovalPolicy, InteractiveApprovalPolicy
4
+ from nexus.security.command_policy import DefaultCommandPolicy
5
+ from nexus.security.executables import TrustedExecutables
6
+ from nexus.security.workspace import WorkspaceGuard
7
+
8
+ __all__ = [
9
+ "AutoApprovalPolicy",
10
+ "DefaultCommandPolicy",
11
+ "InteractiveApprovalPolicy",
12
+ "TrustedExecutables",
13
+ "WorkspaceGuard",
14
+ ]
@@ -0,0 +1,73 @@
1
+ """Interactive and auto approval decisions without terminal input coupling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from datetime import UTC, datetime
7
+
8
+ from nexus.domain.approvals import ApprovalRequest
9
+ from nexus.domain.ports.tooling import InteractiveDecisionCallback
10
+ from nexus.domain.tooling import ApprovalDecision, RiskLevel
11
+ from nexus.errors import PermissionDeniedError, ToolExecutionError
12
+
13
+
14
+ class InteractiveApprovalPolicy:
15
+ def __init__(self, decision_callback: InteractiveDecisionCallback) -> None:
16
+ self._decision_callback = decision_callback
17
+
18
+ async def request(self, request: ApprovalRequest) -> ApprovalRequest:
19
+ _require_pending(request)
20
+ if request.risk_level is RiskLevel.DANGEROUS:
21
+ return _decide(
22
+ request,
23
+ ApprovalDecision.DENIED,
24
+ actor="security_policy",
25
+ reason="Dangerous operations are hard-denied.",
26
+ )
27
+ decision, reason = await self._decision_callback(request)
28
+ if decision not in {ApprovalDecision.APPROVED, ApprovalDecision.DENIED}:
29
+ raise ToolExecutionError(
30
+ "Interactive approval returned an invalid decision.",
31
+ code="INVALID_APPROVAL_TRANSITION",
32
+ )
33
+ return _decide(request, decision, actor="user", reason=reason)
34
+
35
+
36
+ class AutoApprovalPolicy:
37
+ async def request(self, request: ApprovalRequest) -> ApprovalRequest:
38
+ _require_pending(request)
39
+ actor = (
40
+ "security_policy"
41
+ if request.risk_level is RiskLevel.DANGEROUS
42
+ else "auto_policy"
43
+ )
44
+ return _decide(
45
+ request,
46
+ ApprovalDecision.DENIED,
47
+ actor=actor,
48
+ reason="Auto policy cannot authorize this operation.",
49
+ )
50
+
51
+
52
+ def _require_pending(request: ApprovalRequest) -> None:
53
+ if request.decision is not ApprovalDecision.PENDING:
54
+ raise PermissionDeniedError(
55
+ "Only a pending approval can be decided.",
56
+ code="INVALID_APPROVAL_TRANSITION",
57
+ )
58
+
59
+
60
+ def _decide(
61
+ request: ApprovalRequest,
62
+ decision: ApprovalDecision,
63
+ *,
64
+ actor: str,
65
+ reason: str | None,
66
+ ) -> ApprovalRequest:
67
+ return replace(
68
+ request,
69
+ decision=decision,
70
+ actor=actor,
71
+ reason=reason,
72
+ decided_at=datetime.now(UTC),
73
+ )
@@ -0,0 +1,117 @@
1
+ """Centralized SAFE/WRITE/DANGEROUS classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from types import MappingProxyType
7
+ from typing import TypeGuard
8
+
9
+ from nexus.domain.tooling import JsonObject, RiskLevel
10
+ from nexus.security.executables import TrustedExecutables
11
+ from nexus.security.git_commands import is_canonical_git_argv
12
+
13
+ _READ_TOOLS = {"list_files", "search_files", "read_file", "lexical_search"}
14
+ _GIT_TOOLS = {"git_status", "git_diff", "git_log"}
15
+ _EDIT_TOOLS = {"apply_patch", "write_file"}
16
+ _OPERATORS = {"&&", "||", "|", ">", ">>", "<", ";"}
17
+
18
+
19
+ class DefaultCommandPolicy:
20
+ def __init__(
21
+ self,
22
+ executables: TrustedExecutables,
23
+ mcp_risks: Mapping[str, RiskLevel] | None = None,
24
+ ) -> None:
25
+ self._executables = executables
26
+ self._mcp_risks = MappingProxyType(dict(mcp_risks or {}))
27
+
28
+ def classify(self, *, operation: str, arguments: JsonObject) -> RiskLevel:
29
+ if operation in self._mcp_risks:
30
+ return self._mcp_risks[operation]
31
+ if operation in _READ_TOOLS:
32
+ return RiskLevel.SAFE
33
+ if operation in _GIT_TOOLS:
34
+ return self._classify_git(operation, arguments)
35
+ if operation in _EDIT_TOOLS:
36
+ return RiskLevel.WRITE
37
+ if operation != "shell":
38
+ return RiskLevel.DANGEROUS
39
+ argv = arguments.get("argv")
40
+ if not _is_string_list(argv):
41
+ return RiskLevel.DANGEROUS
42
+ normalized = self._executables.normalize_argv(argv)
43
+ if any(item in _OPERATORS for item in normalized):
44
+ return RiskLevel.DANGEROUS
45
+ if self._is_safe_inspection(normalized):
46
+ return RiskLevel.SAFE
47
+ if self._is_controlled_write(normalized):
48
+ return RiskLevel.WRITE
49
+ return RiskLevel.DANGEROUS
50
+
51
+ def _classify_git(self, operation: str, arguments: JsonObject) -> RiskLevel:
52
+ argv = arguments.get("argv")
53
+ if _is_string_list(argv):
54
+ normalized = self._executables.normalize_argv(argv)
55
+ return (
56
+ RiskLevel.SAFE
57
+ if is_canonical_git_argv(operation, normalized, self._executables.git)
58
+ else RiskLevel.DANGEROUS
59
+ )
60
+ if operation == "git_status":
61
+ return RiskLevel.SAFE if arguments == {} else RiskLevel.DANGEROUS
62
+ if operation == "git_diff":
63
+ return (
64
+ RiskLevel.SAFE
65
+ if set(arguments) <= {"staged"}
66
+ and isinstance(arguments.get("staged", False), bool)
67
+ else RiskLevel.DANGEROUS
68
+ )
69
+ value = arguments.get("max_entries", 20)
70
+ return (
71
+ RiskLevel.SAFE
72
+ if set(arguments) <= {"max_entries"}
73
+ and isinstance(value, int)
74
+ and not isinstance(value, bool)
75
+ and 1 <= value <= 100
76
+ else RiskLevel.DANGEROUS
77
+ )
78
+
79
+ def _is_safe_inspection(self, argv: list[str]) -> bool:
80
+ return argv in [
81
+ [self._executables.python, "--version"],
82
+ [self._executables.python, "-V"],
83
+ *(
84
+ [[self._executables.uv, "--version"]]
85
+ if self._executables.uv is not None
86
+ else []
87
+ ),
88
+ *(
89
+ [[self._executables.git, "--version"]]
90
+ if self._executables.git is not None
91
+ else []
92
+ ),
93
+ ]
94
+
95
+ def _is_controlled_write(self, argv: list[str]) -> bool:
96
+ direct = {
97
+ self._executables.pytest,
98
+ self._executables.ruff,
99
+ self._executables.mypy,
100
+ }
101
+ if argv[0] in direct - {None}:
102
+ return argv[0] != self._executables.ruff or argv[1:2] == ["check"]
103
+ uv = self._executables.uv
104
+ if uv is None or argv[0] != uv:
105
+ return False
106
+ if argv[1:2] == ["build"]:
107
+ return True
108
+ return len(argv) >= 3 and argv[1] == "run" and argv[2] in {
109
+ "pytest",
110
+ "mypy",
111
+ } or len(argv) >= 4 and argv[1:3] == ["run", "ruff"] and argv[3] == "check"
112
+
113
+
114
+ def _is_string_list(value: object) -> TypeGuard[list[str]]:
115
+ return isinstance(value, list) and bool(value) and all(
116
+ isinstance(item, str) and bool(item) for item in value
117
+ )
@@ -0,0 +1,107 @@
1
+ """Resolve a small trusted executable set without trusting the workspace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from nexus.errors import ToolExecutionError
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class TrustedExecutables:
15
+ python: str
16
+ uv: str | None
17
+ git: str | None
18
+ pytest: str | None
19
+ ruff: str | None
20
+ mypy: str | None
21
+
22
+ @classmethod
23
+ def resolve(cls, workspace: Path) -> TrustedExecutables:
24
+ sanitized_path = _sanitized_path(workspace)
25
+ python = _which_outside_workspace("python", workspace, sanitized_path)
26
+ if python is None:
27
+ raise ToolExecutionError("A trusted Python executable is unavailable.")
28
+ return cls(
29
+ python=python,
30
+ uv=_which_outside_workspace("uv", workspace, sanitized_path),
31
+ git=_which_outside_workspace("git", workspace, sanitized_path),
32
+ pytest=_which_outside_workspace("pytest", workspace, sanitized_path),
33
+ ruff=_which_outside_workspace("ruff", workspace, sanitized_path),
34
+ mypy=_which_outside_workspace("mypy", workspace, sanitized_path),
35
+ )
36
+
37
+ def normalize_argv(self, argv: list[str]) -> list[str]:
38
+ if not argv:
39
+ return []
40
+ resolved = self._resolve_requested(argv[0])
41
+ return [resolved or argv[0], *argv[1:]]
42
+
43
+ def _resolve_requested(self, requested: str) -> str | None:
44
+ requested_path = Path(requested)
45
+ normalized_name = requested_path.name.casefold()
46
+ if normalized_name.endswith(".exe"):
47
+ normalized_name = normalized_name[:-4]
48
+ candidates = {
49
+ "python": self.python,
50
+ "uv": self.uv,
51
+ "git": self.git,
52
+ "pytest": self.pytest,
53
+ "ruff": self.ruff,
54
+ "mypy": self.mypy,
55
+ }
56
+ candidate = candidates.get(normalized_name)
57
+ if candidate is None:
58
+ return None
59
+ if requested_path.is_absolute():
60
+ try:
61
+ if os.path.normcase(str(requested_path.resolve(strict=True))) != os.path.normcase(
62
+ candidate
63
+ ):
64
+ return None
65
+ except OSError:
66
+ return None
67
+ return candidate
68
+
69
+
70
+ def _which_outside_workspace(
71
+ name: str,
72
+ workspace: Path,
73
+ search_path: str,
74
+ ) -> str | None:
75
+ located = shutil.which(name, path=search_path)
76
+ if located is None:
77
+ return None
78
+ resolved = Path(located).resolve(strict=True)
79
+ return None if _is_within(resolved, workspace) else str(resolved)
80
+
81
+
82
+ def _sanitized_path(workspace: Path) -> str:
83
+ entries: list[str] = []
84
+ for value in os.environ.get("PATH", "").split(os.pathsep):
85
+ if not value:
86
+ continue
87
+ candidate = Path(value)
88
+ if not candidate.is_absolute():
89
+ continue
90
+ try:
91
+ resolved = candidate.resolve(strict=True)
92
+ except OSError:
93
+ continue
94
+ if resolved.is_dir() and not _is_within(resolved, workspace):
95
+ entries.append(str(resolved))
96
+ return os.pathsep.join(entries)
97
+
98
+
99
+ def _is_within(path: Path, workspace: Path) -> bool:
100
+ normalized_path = os.path.normcase(str(path))
101
+ normalized_workspace = os.path.normcase(str(workspace.resolve(strict=True)))
102
+ try:
103
+ return os.path.commonpath([normalized_path, normalized_workspace]) == (
104
+ normalized_workspace
105
+ )
106
+ except ValueError:
107
+ return False
@@ -0,0 +1,65 @@
1
+ """Canonical read-only Git argv construction and matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def status_argv(git: str) -> list[str]:
7
+ return [
8
+ git,
9
+ "-c",
10
+ "core.fsmonitor=false",
11
+ "--no-pager",
12
+ "status",
13
+ "--short",
14
+ "--branch",
15
+ ]
16
+
17
+
18
+ def diff_argv(git: str, *, staged: bool) -> list[str]:
19
+ argv = [
20
+ git,
21
+ "-c",
22
+ "core.fsmonitor=false",
23
+ "--no-pager",
24
+ "diff",
25
+ "--no-ext-diff",
26
+ "--no-textconv",
27
+ ]
28
+ if staged:
29
+ argv.append("--cached")
30
+ return argv
31
+
32
+
33
+ def log_argv(git: str, *, max_entries: int) -> list[str]:
34
+ return [
35
+ git,
36
+ "-c",
37
+ "core.fsmonitor=false",
38
+ "--no-pager",
39
+ "log",
40
+ f"--max-count={max_entries}",
41
+ "--oneline",
42
+ "--no-decorate",
43
+ ]
44
+
45
+
46
+ def is_canonical_git_argv(operation: str, argv: list[str], git: str | None) -> bool:
47
+ if git is None:
48
+ return False
49
+ if operation == "git_status":
50
+ return argv == status_argv(git)
51
+ if operation == "git_diff":
52
+ return argv in (diff_argv(git, staged=False), diff_argv(git, staged=True))
53
+ if operation == "git_log" and len(argv) == 8:
54
+ prefix = [git, "-c", "core.fsmonitor=false", "--no-pager", "log"]
55
+ if argv[:5] != prefix or argv[6:] != ["--oneline", "--no-decorate"]:
56
+ return False
57
+ value = argv[5]
58
+ if not value.startswith("--max-count="):
59
+ return False
60
+ try:
61
+ count = int(value.removeprefix("--max-count="))
62
+ except ValueError:
63
+ return False
64
+ return 1 <= count <= 100
65
+ return False
@@ -0,0 +1,93 @@
1
+ """Centralized repository-relative path containment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from nexus.errors import PermissionDeniedError, ToolExecutionError
9
+
10
+
11
+ class WorkspaceGuard:
12
+ def __init__(self, workspace: Path) -> None:
13
+ try:
14
+ self._workspace = workspace.resolve(strict=True)
15
+ except OSError as exc:
16
+ raise ToolExecutionError("The configured workspace does not exist.") from exc
17
+ if not self._workspace.is_dir():
18
+ raise ToolExecutionError("The configured workspace is not a directory.")
19
+ self._normalized_workspace = os.path.normcase(str(self._workspace))
20
+
21
+ @property
22
+ def root(self) -> Path:
23
+ return self._workspace
24
+
25
+ def resolve_existing(
26
+ self,
27
+ relative_path: str,
28
+ *,
29
+ require_file: bool = False,
30
+ require_directory: bool = False,
31
+ ) -> Path:
32
+ requested = Path(relative_path)
33
+ if requested.is_absolute():
34
+ raise self._denied()
35
+ try:
36
+ resolved = (self._workspace / requested).resolve(strict=True)
37
+ except (OSError, RuntimeError) as exc:
38
+ if ".." in requested.parts:
39
+ raise self._denied() from exc
40
+ raise ToolExecutionError(
41
+ "The requested workspace path does not exist.",
42
+ code="TOOL_EXECUTION_ERROR",
43
+ ) from exc
44
+ if not self._contains(resolved):
45
+ raise self._denied()
46
+ if require_file and not resolved.is_file():
47
+ raise ToolExecutionError("The requested path is not a regular file.")
48
+ if require_directory and not resolved.is_dir():
49
+ raise ToolExecutionError("The requested path is not a directory.")
50
+ return resolved
51
+
52
+ def relative_display(self, path: Path) -> str:
53
+ return path.relative_to(self._workspace).as_posix() or "."
54
+
55
+ def resolve_for_creation(self, relative_path: str) -> Path:
56
+ requested = Path(relative_path)
57
+ if requested.is_absolute() or ".." in requested.parts:
58
+ raise self._denied()
59
+ parent_requested = requested.parent
60
+ try:
61
+ parent = (self._workspace / parent_requested).resolve(strict=True)
62
+ except (OSError, RuntimeError) as exc:
63
+ if ".." in requested.parts:
64
+ raise self._denied() from exc
65
+ raise ToolExecutionError(
66
+ "The target parent directory does not exist.",
67
+ code="PARENT_DIRECTORY_MISSING",
68
+ ) from exc
69
+ if not self._contains(parent) or not parent.is_dir():
70
+ raise self._denied()
71
+ target = parent / requested.name
72
+ if target.exists() or target.is_symlink():
73
+ raise ToolExecutionError(
74
+ "The target file already exists.",
75
+ code="FILE_ALREADY_EXISTS",
76
+ )
77
+ return target
78
+
79
+ def _contains(self, path: Path) -> bool:
80
+ normalized = os.path.normcase(str(path))
81
+ try:
82
+ return os.path.commonpath([self._normalized_workspace, normalized]) == (
83
+ self._normalized_workspace
84
+ )
85
+ except ValueError:
86
+ return False
87
+
88
+ @staticmethod
89
+ def _denied() -> PermissionDeniedError:
90
+ return PermissionDeniedError(
91
+ "The requested path is outside the configured workspace.",
92
+ code="WORKSPACE_PATH_DENIED",
93
+ )
@@ -0,0 +1,7 @@
1
+ """File-based Day 7 Skill services."""
2
+
3
+ from nexus.skills.loader import FileSkillLoader
4
+ from nexus.skills.registry import DefaultSkillRegistry
5
+ from nexus.skills.selector import ModelSkillSelector
6
+
7
+ __all__ = ["DefaultSkillRegistry", "FileSkillLoader", "ModelSkillSelector"]
@@ -0,0 +1 @@
1
+ """Packaged Nexus builtin Skill resources."""
@@ -0,0 +1,26 @@
1
+ +++
2
+ id = "debug-python"
3
+ name = "Debug Python"
4
+ description = "Diagnose Python failures from reproducible repository evidence."
5
+ usage_scenario = "Use for Python exceptions, failing tests, and behavioral regressions."
6
+ priority = 60
7
+ version = "1.0.0"
8
+ +++
9
+
10
+ ## Execution Principles
11
+
12
+ Reproduce the failure first and keep observed evidence separate from hypotheses.
13
+
14
+ ## Recommended Tools
15
+
16
+ Use bounded repository search, focused file reads, and the approved validation tools.
17
+
18
+ ## Workflow
19
+
20
+ 1. Reproduce the smallest failing case.
21
+ 2. Trace the narrowest relevant call path.
22
+ 3. Correct the evidenced cause and rerun focused validation.
23
+
24
+ ## Constraints
25
+
26
+ Do not broaden scope, suppress failures, or bypass Plan, policy, sandbox, and validation.
@@ -0,0 +1,26 @@
1
+ +++
2
+ id = "review-repository"
3
+ name = "Review Repository"
4
+ description = "Review repository changes for correctness, regressions, and contract drift."
5
+ usage_scenario = "Use for code review, architecture review, and change-risk assessment."
6
+ priority = 40
7
+ version = "1.0.0"
8
+ +++
9
+
10
+ ## Execution Principles
11
+
12
+ Lead with concrete findings and verify each claim against the current diff and contracts.
13
+
14
+ ## Recommended Tools
15
+
16
+ Use bounded search, focused source reads, git status/diff, and relevant validation evidence.
17
+
18
+ ## Workflow
19
+
20
+ 1. Establish the intended contract and change boundary.
21
+ 2. Inspect affected call paths and tests for behavioral risk.
22
+ 3. Report actionable findings by severity with precise evidence.
23
+
24
+ ## Constraints
25
+
26
+ Do not invent defects, expose private reasoning, or treat successful tests as complete proof.
@@ -0,0 +1,26 @@
1
+ +++
2
+ id = "write-tests"
3
+ name = "Write Tests"
4
+ description = "Add focused tests that demonstrate behavior and guard regressions."
5
+ usage_scenario = "Use when a task primarily asks for unit, integration, or regression tests."
6
+ priority = 50
7
+ version = "1.0.0"
8
+ +++
9
+
10
+ ## Execution Principles
11
+
12
+ Test observable contracts and failure boundaries rather than private implementation details.
13
+
14
+ ## Recommended Tools
15
+
16
+ Use focused repository reads, the existing test framework, and approved validation commands.
17
+
18
+ ## Workflow
19
+
20
+ 1. Identify the public behavior and existing test conventions.
21
+ 2. Add the smallest deterministic fixture that fails for the missing behavior.
22
+ 3. Run focused tests before the broader regression suite.
23
+
24
+ ## Constraints
25
+
26
+ Do not weaken assertions, replace real evidence with mocks unnecessarily, or bypass policy.