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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
)
|
nexus/skills/__init__.py
ADDED
|
@@ -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.
|