Skip to content
Open
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ ENV=dev # options: dev|s
# Options: openai | anthropic | anthropic_proxy | nv_build
SKILLSPECTOR_PROVIDER=

# Aggregate deadline for one complete scan workflow. Defaults to 60 seconds;
# raise this for large skills that require a complete rather than partial scan.
# SKILLSPECTOR_MAX_WORKFLOW_SECONDS=120

# Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or
# leave SKILLSPECTOR_PROVIDER unset and set NVIDIA_INFERENCE_KEY for the
# default nv_build path).
Expand Down
10 changes: 10 additions & 0 deletions docs/ANALYSIS_RESOURCE_BOUNDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,13 @@ When relevant analysis is incomplete:

A low score or zero findings must not be interpreted as complete coverage when
`analysis_completeness.is_complete` is false.


## Configuring the aggregate workflow deadline

The scanner limits one complete workflow to 60 seconds by default. Set
`SKILLSPECTOR_MAX_WORKFLOW_SECONDS` to a positive finite number of seconds to
raise that aggregate deadline for large skills. The setting applies to direct,
CLI, recursive, and multi-skill scans; byte and artifact ceilings remain in
effect. Invalid, zero, negative, infinite, or NaN values safely keep the
60-second default.
4 changes: 2 additions & 2 deletions src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
from skillspector.multi_skill import MultiSkillDetectionResult, SkillDirectory, detect_skills
from skillspector.nodes.report import report
from skillspector.sarif_models import SARIF_SCHEMA_URI, validate_sarif_report
from skillspector.state import MAX_WORKFLOW_BYTES
from skillspector.state import MAX_WORKFLOW_BYTES, MAX_WORKFLOW_SECONDS
from skillspector.suppression import (
Baseline,
build_baseline_dict,
Expand Down Expand Up @@ -100,7 +100,7 @@ def _ensure_utf8_streams() -> None:

_TRANSITIVE_MAX_TARGETS = 32
_TRANSITIVE_MAX_BYTES = 10 * 1024 * 1024
_TRANSITIVE_MAX_SECONDS = 60.0
_TRANSITIVE_MAX_SECONDS = MAX_WORKFLOW_SECONDS
_TRANSITIVE_MAX_ARTIFACTS = 10_000
_TRANSITIVE_MAX_FINDINGS = 10_000
_TRANSITIVE_MAX_COMPONENTS = 10_000
Expand Down
35 changes: 34 additions & 1 deletion src/skillspector/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

from __future__ import annotations

import math
import operator
import os
from dataclasses import dataclass, field
from time import monotonic
from typing import Annotated, NotRequired
Expand All @@ -36,9 +38,40 @@
LedgerRecordType,
ledger_event,
)
from skillspector.logging_config import get_logger
from skillspector.models import Finding

MAX_WORKFLOW_SECONDS = 60.0
logger = get_logger(__name__)

DEFAULT_MAX_WORKFLOW_SECONDS = 60.0


def _workflow_max_seconds_from_environment(value: str | None) -> float:
"""Return a positive finite workflow deadline or the safe default."""
if value is None:
return DEFAULT_MAX_WORKFLOW_SECONDS
try:
seconds = float(value)
except ValueError:
logger.warning(
"SKILLSPECTOR_MAX_WORKFLOW_SECONDS=%r is not numeric, using default %.1fs",
value,
DEFAULT_MAX_WORKFLOW_SECONDS,
)
return DEFAULT_MAX_WORKFLOW_SECONDS
if not math.isfinite(seconds) or seconds <= 0:
logger.warning(
"SKILLSPECTOR_MAX_WORKFLOW_SECONDS=%r must be finite and positive, using default %.1fs",
value,
DEFAULT_MAX_WORKFLOW_SECONDS,
)
return DEFAULT_MAX_WORKFLOW_SECONDS
return seconds


MAX_WORKFLOW_SECONDS = _workflow_max_seconds_from_environment(
os.environ.get("SKILLSPECTOR_MAX_WORKFLOW_SECONDS")
)
MAX_WORKFLOW_BYTES = 64 * 1024 * 1024
MAX_WORKFLOW_ARTIFACTS = 10_000
MAX_WORKFLOW_LIMITATION_RECORDS = 256
Expand Down
18 changes: 18 additions & 0 deletions tests/nodes/test_build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
from skillspector.providers.openai import OpenAIProvider
from skillspector.python_ast import ParsedPythonFile, get_python_ast
from skillspector.state import (
DEFAULT_MAX_WORKFLOW_SECONDS,
MAX_WORKFLOW_ARTIFACTS,
MAX_WORKFLOW_BYTES,
MAX_WORKFLOW_SECONDS,
SkillspectorState,
WorkflowResourceBudget,
_workflow_max_seconds_from_environment,
)

_OMS_FIXTURE = Path(__file__).parents[1] / "fixtures" / "oms" / "mcore-split-pr.skill.oms.sig"
Expand Down Expand Up @@ -188,6 +190,22 @@ def test_build_context_starts_and_returns_default_graph_wide_budget(tmp_path: Pa
assert budget.scanned_artifacts == 1


@pytest.mark.parametrize(
("value", "expected"),
[
(None, DEFAULT_MAX_WORKFLOW_SECONDS),
("120", 120.0),
("0.5", 0.5),
("0", DEFAULT_MAX_WORKFLOW_SECONDS),
("-1", DEFAULT_MAX_WORKFLOW_SECONDS),
("nan", DEFAULT_MAX_WORKFLOW_SECONDS),
("not-a-number", DEFAULT_MAX_WORKFLOW_SECONDS),
],
)
def test_workflow_budget_seconds_environment_parsing(value: str | None, expected: float) -> None:
assert _workflow_max_seconds_from_environment(value) == expected


def test_build_context_reuses_supplied_stricter_transitive_budget(tmp_path: Path) -> None:
from skillspector.cli import _TransitiveBudget, _TransitiveTraversalState

Expand Down
Loading