Skip to content

Commit 88eedca

Browse files
Merge pull request #468 from deepujain/fix/460-workflow-budget
feat: make workflow deadline configurable SkillEvaluator should have a better timeout considering the bump to 600sec for skillspector this change
2 parents 7805bb9 + a3041b8 commit 88eedca

6 files changed

Lines changed: 76 additions & 8 deletions

File tree

‎.env.example‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ ENV=dev # options: dev|s
55
# Options: openai | anthropic | anthropic_proxy | nv_build
66
SKILLSPECTOR_PROVIDER=
77

8+
# Aggregate deadline for one complete scan workflow. Defaults to 600 seconds;
9+
# override this positive finite value when a different limit is required.
10+
# SKILLSPECTOR_MAX_WORKFLOW_SECONDS=600
11+
812
# Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or
913
# leave SKILLSPECTOR_PROVIDER unset and set NVIDIA_INFERENCE_KEY for the
1014
# default nv_build path).

‎docs/ANALYSIS_RESOURCE_BOUNDS.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Values below are implementation defaults. MiB means 1,048,576 bytes.
1616
| Discovery time | 30 seconds | One skill bundle |
1717
| Canonical cached source bytes | 64 MiB | Ordinary files and expanded nested members combined |
1818
| Cached bytes from one filesystem artifact | 16 MiB | One artifact |
19-
| End-to-end workflow time | 60 seconds | One graph execution |
19+
| End-to-end workflow time | 600 seconds | One graph execution |
2020
| Cache and context-processing time | 60 seconds | One skill bundle, within the workflow deadline |
2121

2222
The 64 MiB ceiling accounts for the canonical raw bytes retained for analysis. Local decoded text
@@ -130,7 +130,7 @@ work consume the same allowance.
130130
| External targets | 32 | One traversal |
131131
| Downloaded and cached source bytes | 10 MiB | Root and dependencies combined |
132132
| Discovered and expanded artifacts | 10,000 | Root and dependencies combined |
133-
| Traversal time | 60 seconds | Root and dependencies combined |
133+
| Traversal time | 600 seconds | Root and dependencies combined |
134134
| Reference source records | 1,024 | One extraction |
135135
| Reference source bytes | 1,000,000 | One extraction |
136136
| Raw reference candidates | 4,096 | One extraction |
@@ -211,3 +211,13 @@ When relevant analysis is incomplete:
211211

212212
A low score or zero findings must not be interpreted as complete coverage when
213213
`analysis_completeness.is_complete` is false.
214+
215+
216+
## Configuring the aggregate workflow deadline
217+
218+
The scanner limits one complete workflow to 600 seconds by default. Set
219+
`SKILLSPECTOR_MAX_WORKFLOW_SECONDS` to a positive finite number of seconds to
220+
change that aggregate deadline. The setting applies to direct,
221+
CLI, recursive, and multi-skill scans; byte and artifact ceilings remain in
222+
effect. Invalid, zero, negative, infinite, or NaN values safely keep the
223+
600-second default.

‎src/skillspector/cli.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from skillspector.multi_skill import MultiSkillDetectionResult, SkillDirectory, detect_skills
5757
from skillspector.nodes.report import report
5858
from skillspector.sarif_models import SARIF_SCHEMA_URI, validate_sarif_report
59-
from skillspector.state import MAX_WORKFLOW_BYTES
59+
from skillspector.state import MAX_WORKFLOW_BYTES, MAX_WORKFLOW_SECONDS
6060
from skillspector.suppression import (
6161
Baseline,
6262
build_baseline_dict,
@@ -100,7 +100,7 @@ def _ensure_utf8_streams() -> None:
100100

101101
_TRANSITIVE_MAX_TARGETS = 32
102102
_TRANSITIVE_MAX_BYTES = 10 * 1024 * 1024
103-
_TRANSITIVE_MAX_SECONDS = 60.0
103+
_TRANSITIVE_MAX_SECONDS = MAX_WORKFLOW_SECONDS
104104
_TRANSITIVE_MAX_ARTIFACTS = 10_000
105105
_TRANSITIVE_MAX_FINDINGS = 10_000
106106
_TRANSITIVE_MAX_COMPONENTS = 10_000

‎src/skillspector/state.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
from __future__ import annotations
1919

20+
import math
2021
import operator
22+
import os
2123
from dataclasses import dataclass, field
2224
from time import monotonic
2325
from typing import Annotated, NotRequired
@@ -36,9 +38,40 @@
3638
LedgerRecordType,
3739
ledger_event,
3840
)
41+
from skillspector.logging_config import get_logger
3942
from skillspector.models import Finding
4043

41-
MAX_WORKFLOW_SECONDS = 60.0
44+
logger = get_logger(__name__)
45+
46+
DEFAULT_MAX_WORKFLOW_SECONDS = 600.0
47+
48+
49+
def _workflow_max_seconds_from_environment(value: str | None) -> float:
50+
"""Return a positive finite workflow deadline or the safe default."""
51+
if value is None:
52+
return DEFAULT_MAX_WORKFLOW_SECONDS
53+
try:
54+
seconds = float(value)
55+
except ValueError:
56+
logger.warning(
57+
"SKILLSPECTOR_MAX_WORKFLOW_SECONDS=%r is not numeric, using default %.1fs",
58+
value,
59+
DEFAULT_MAX_WORKFLOW_SECONDS,
60+
)
61+
return DEFAULT_MAX_WORKFLOW_SECONDS
62+
if not math.isfinite(seconds) or seconds <= 0:
63+
logger.warning(
64+
"SKILLSPECTOR_MAX_WORKFLOW_SECONDS=%r must be finite and positive, using default %.1fs",
65+
value,
66+
DEFAULT_MAX_WORKFLOW_SECONDS,
67+
)
68+
return DEFAULT_MAX_WORKFLOW_SECONDS
69+
return seconds
70+
71+
72+
MAX_WORKFLOW_SECONDS = _workflow_max_seconds_from_environment(
73+
os.environ.get("SKILLSPECTOR_MAX_WORKFLOW_SECONDS")
74+
)
4275
MAX_WORKFLOW_BYTES = 64 * 1024 * 1024
4376
MAX_WORKFLOW_ARTIFACTS = 10_000
4477
MAX_WORKFLOW_LIMITATION_RECORDS = 256

‎tests/nodes/test_build_context.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
from skillspector.providers.openai import OpenAIProvider
3939
from skillspector.python_ast import ParsedPythonFile, get_python_ast
4040
from skillspector.state import (
41+
DEFAULT_MAX_WORKFLOW_SECONDS,
4142
MAX_WORKFLOW_ARTIFACTS,
4243
MAX_WORKFLOW_BYTES,
4344
MAX_WORKFLOW_SECONDS,
4445
SkillspectorState,
4546
WorkflowResourceBudget,
47+
_workflow_max_seconds_from_environment,
4648
)
4749

4850
_OMS_FIXTURE = Path(__file__).parents[1] / "fixtures" / "oms" / "mcore-split-pr.skill.oms.sig"
@@ -180,14 +182,31 @@ def test_build_context_starts_and_returns_default_graph_wide_budget(tmp_path: Pa
180182

181183
budget = result["workflow_resource_budget"]
182184
assert isinstance(budget, WorkflowResourceBudget)
183-
assert budget.max_seconds == MAX_WORKFLOW_SECONDS == 60.0
185+
assert DEFAULT_MAX_WORKFLOW_SECONDS == 600.0
186+
assert budget.max_seconds == MAX_WORKFLOW_SECONDS
184187
assert budget.max_bytes == MAX_WORKFLOW_BYTES == 64 * 1024 * 1024
185188
assert budget.max_artifacts == MAX_WORKFLOW_ARTIFACTS == 10_000
186189
assert budget.started_at is not None
187190
assert budget.scanned_bytes == len(payload)
188191
assert budget.scanned_artifacts == 1
189192

190193

194+
@pytest.mark.parametrize(
195+
("value", "expected"),
196+
[
197+
(None, DEFAULT_MAX_WORKFLOW_SECONDS),
198+
("120", 120.0),
199+
("0.5", 0.5),
200+
("0", DEFAULT_MAX_WORKFLOW_SECONDS),
201+
("-1", DEFAULT_MAX_WORKFLOW_SECONDS),
202+
("nan", DEFAULT_MAX_WORKFLOW_SECONDS),
203+
("not-a-number", DEFAULT_MAX_WORKFLOW_SECONDS),
204+
],
205+
)
206+
def test_workflow_budget_seconds_environment_parsing(value: str | None, expected: float) -> None:
207+
assert _workflow_max_seconds_from_environment(value) == expected
208+
209+
191210
def test_build_context_reuses_supplied_stricter_transitive_budget(tmp_path: Path) -> None:
192211
from skillspector.cli import _TransitiveBudget, _TransitiveTraversalState
193212

‎tests/unit/test_cli.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,14 +1760,16 @@ def fake_run_graph_scan(
17601760
show_suppressed: bool = False,
17611761
transitive_traversal=None,
17621762
) -> dict[str, object]:
1763-
fake_time["value"] += 61.0
1763+
fake_time["value"] += cli._TRANSITIVE_MAX_SECONDS + 1.0
17641764
return _mock_graph_result(file_cache={"SKILL.md": "https://github.com/org/dep.git"})
17651765

17661766
def fake_scan_transitive(*args, traversal=None, **kwargs) -> dict[str, object]:
17671767
assert traversal is not None
17681768
assert traversal.remaining_seconds() == 0.0
17691769
assert traversal.can_scan_more() is False
1770-
assert traversal.truncation_reasons == ["time budget 60s reached"]
1770+
assert traversal.truncation_reasons == [
1771+
f"time budget {cli._TRANSITIVE_MAX_SECONDS:g}s reached"
1772+
]
17711773
return {
17721774
"report_body": "{}",
17731775
"filtered_findings": [],

0 commit comments

Comments
 (0)