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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] — since 2026-06-08

### Added

#### New Nodes
- **Steadwing RCA** — agent tool exposing Steadwing's AI root-cause analysis (`run_rca`). Given an error or stack trace (and optional source files), it opens a Steadwing investigation that correlates logs, metrics, traces, and code across the stack, and returns the investigation URL. REST-backed (`POST /api/mcp/analyze`, `X-API-Key`) with a secure API key (env fallback `ROCKETRIDE_STEADWING_KEY`). Experimental V0. (#1248)
Comment on lines +12 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a blank line after the heading to satisfy markdownlint.

Line 12 should be followed by an empty line before the list at Line 13 (MD022).

Suggested diff
 #### New Nodes
+
 - **Steadwing RCA** — agent tool exposing Steadwing's AI root-cause analysis (`run_rca`). Given an error or stack trace (and optional source files), it opens a Steadwing investigation that correlates logs, metrics, traces, and code across the stack, and returns the investigation URL. REST-backed (`POST /api/mcp/analyze`, `X-API-Key`) with a secure API key (env fallback `ROCKETRIDE_STEADWING_KEY`). Experimental V0. (`#1248`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### New Nodes
- **Steadwing RCA** — agent tool exposing Steadwing's AI root-cause analysis (`run_rca`). Given an error or stack trace (and optional source files), it opens a Steadwing investigation that correlates logs, metrics, traces, and code across the stack, and returns the investigation URL. REST-backed (`POST /api/mcp/analyze`, `X-API-Key`) with a secure API key (env fallback `ROCKETRIDE_STEADWING_KEY`). Experimental V0. (#1248)
#### New Nodes
- **Steadwing RCA** — agent tool exposing Steadwing's AI root-cause analysis (`run_rca`). Given an error or stack trace (and optional source files), it opens a Steadwing investigation that correlates logs, metrics, traces, and code across the stack, and returns the investigation URL. REST-backed (`POST /api/mcp/analyze`, `X-API-Key`) with a secure API key (env fallback `ROCKETRIDE_STEADWING_KEY`). Experimental V0. (`#1248`)
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 12-12: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 12 - 13, Insert a blank line after the "#### New
Nodes" heading in CHANGELOG.md so the list item starting with "- **Steadwing
RCA**" is separated by an empty line (fixing MD022); locate the heading "####
New Nodes" and add one newline before the list entry.

Source: Linters/SAST tools


## [3.3.0] - 2026-06-08

### ⚠ Breaking Changes — Client SDKs (`rocketride` / `rocketride-python`)
Expand Down
74 changes: 74 additions & 0 deletions nodes/src/nodes/tool_steadwing/IGlobal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

"""
Steadwing tool node - global (shared) state.

Reads the Steadwing API key from the node config (or the ROCKETRIDE_STEADWING_KEY
env var). The root-cause-analysis tool itself lives on IInstance via @tool_function.
"""

from __future__ import annotations

import os

from ai.common.config import Config
from rocketlib import IGlobalBase, OPEN_MODE, error, warning

# Pipeline env vars must be ROCKETRIDE_-prefixed (only those are substituted, and
# the node-test framework maps ROCKETRIDE_<PROVIDER>_<ATTR> -> config).
STEADWING_API_KEY_ENV = 'ROCKETRIDE_STEADWING_KEY'


class IGlobal(IGlobalBase):
"""Global state for tool_steadwing."""

apikey: str = ''

def beginGlobal(self) -> None:
if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG:
return

cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)

apikey = str(cfg.get('apikey') or '').strip() or os.environ.get(STEADWING_API_KEY_ENV, '').strip()

if not apikey:
error(f'tool_steadwing: apikey is required — set it in node config or the {STEADWING_API_KEY_ENV} env var')
raise ValueError('tool_steadwing: apikey is required')

self.apikey = apikey

def validateConfig(self) -> None:
try:
cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)
apikey = str(cfg.get('apikey') or '').strip() or os.environ.get(STEADWING_API_KEY_ENV, '').strip()
if not apikey:
warning('apikey is required')
except Exception as e:
Comment on lines +64 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

validateConfig() will warn on valid saved configs.

Line 67 reads cfg.get('apikey') during validation even though this field is declared secure in services.json. In this repo, secure node fields are not reliably available in validate-time contexts, so a node with a saved API key will still emit the "apikey is required" warning unless the env var is set. Restrict validateConfig() to the env fallback, or suppress the missing-key warning here and leave the hard failure to beginGlobal().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nodes/src/nodes/tool_steadwing/IGlobal.py` around lines 64 - 70, The
validateConfig method currently reads Config.getNodeConfig and checks
cfg.get('apikey'), which triggers false warnings because secure fields aren't
available at validation time; update validateConfig (the validateConfig method
in IGlobal) to NOT rely on cfg.get('apikey') — either only check the environment
variable STEADWING_API_KEY_ENV for an API key fallback or suppress the "apikey
is required" warning here and defer strict validation to beginGlobal(), ensuring
Config.getNodeConfig is not used to decide presence of secure apikeys during
validateConfig.

warning(str(e))

def endGlobal(self) -> None:
self.apikey = ''
207 changes: 207 additions & 0 deletions nodes/src/nodes/tool_steadwing/IInstance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

"""
Steadwing tool node instance.

Exposes ``run_rca`` as a @tool_function: given an error / stack trace (and optional
source files), it calls Steadwing's root-cause-analysis API and returns the URL of
the Steadwing investigation where the cross-tool RCA runs.
"""

from __future__ import annotations

from typing import Any, Dict, List

from rocketlib import IInstanceBase, tool_function, warning

from ai.common.utils import normalize_tool_input, post_with_retry

from .IGlobal import IGlobal

# ---------------------------------------------------------------------------
# Steadwing API configuration
# ---------------------------------------------------------------------------

STEADWING_API_BASE = 'https://api.steadwing.com'
STEADWING_ANALYZE_ENDPOINT = f'{STEADWING_API_BASE}/api/mcp/analyze'
STEADWING_REQUEST_TIMEOUT = 60 # seconds
MAX_FILES = 20


class IInstance(IInstanceBase):
"""Node instance exposing Steadwing root-cause analysis as an agent tool."""

IGlobal: IGlobal

@tool_function(
input_schema={
'type': 'object',
'required': ['error'],
'properties': {
'error': {
'type': 'string',
'description': (
'The complete error message, stack trace, or incident description to '
'analyze. Include as much context as possible — error type, message, '
'stack trace, line numbers, and what was happening when it occurred.'
),
},
'files': {
'type': 'array',
'description': (
'Optional source files that give the analysis context (max 20). Start with '
'the file named in the stack trace, then its direct imports and any relevant '
'configuration.'
),
'items': {
'type': 'object',
'required': ['name', 'content'],
'properties': {
'name': {
'type': 'string',
'description': 'Relative path from the project root, e.g. "src/app.js".',
},
'content': {
'type': 'string',
'description': 'Complete, unmodified file contents.',
},
},
},
},
},
},
output_schema={
'type': 'object',
'properties': {
'success': {'type': 'boolean'},
'incident_url': {
'type': 'string',
'description': 'URL of the Steadwing investigation where the RCA runs.',
},
'message': {'type': 'string'},
},
},
description=(
'Run AI-powered root-cause analysis on a production error or incident using Steadwing. '
'Provide the full error message / stack trace as `error`, and the relevant source `files` '
'when you have them. Steadwing correlates logs, metrics, traces, and code across the stack '
'and returns the URL of an investigation that then runs in the background. Call this ONCE '
'per incident — the returned URL is the deliverable; do not call it again or wait for more.'
),
)
def run_rca(self, args):
"""Open a Steadwing root-cause analysis for an error / incident."""
args = normalize_tool_input(args, tool_name='steadwing')

error_text = (args.get('error') or '').strip()
if not error_text:
raise ValueError('error is required and must be a non-empty string')

payload: Dict[str, Any] = {'error_log': error_text}
files = _normalize_files(args.get('files'))
if files:
payload['files'] = files

headers = {
'accept': 'application/json',
'content-type': 'application/json',
'X-API-Key': self.IGlobal.apikey,
}

resp = post_with_retry(
STEADWING_ANALYZE_ENDPOINT, headers=headers, json=payload, timeout=STEADWING_REQUEST_TIMEOUT
)
Comment on lines +134 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate post_with_retry definition(s):"
rg -n --type=py "def post_with_retry\("

echo
echo "Show retry logic around post_with_retry:"
rg -n -A80 -B20 --type=py "def post_with_retry\("

echo
echo "Find POST call sites using post_with_retry (including Steadwing):"
rg -n --type=py "post_with_retry\("

echo
echo "Inspect Steadwing call site context:"
rg -n -A20 -B10 --type=py "STEADWING_ANALYZE_ENDPOINT|run_rca|post_with_retry"

Repository: rocketride-org/rocketride-server

Length of output: 50388


Steadwing analyze POST can be retried—needs idempotency guard

post_with_retry retries non-2xx POSTs only for transport failures (timeouts/connection errors) and for 429/5xx (it raises immediately for other 4xx). Since this call triggers an async Steadwing investigation, a retry could create multiple investigations if the first request already reached Steadwing but the client observed one of those retryable conditions.

At nodes/src/nodes/tool_steadwing/IInstance.py (lines 134-136), the request uses post_with_retry(...) without any idempotency token/header:

resp = post_with_retry(
    STEADWING_ANALYZE_ENDPOINT, headers=headers, json=payload, timeout=STEADWING_REQUEST_TIMEOUT
)

Please confirm the Steadwing /api/mcp/analyze endpoint de-dupes these retries, or add an idempotency token/guard (or disable retries) for this specific call path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nodes/src/nodes/tool_steadwing/IInstance.py` around lines 134 - 136, This
POST can create duplicate Steadwing investigations when retried; add an
idempotency guard by generating a unique idempotency token (e.g., uuid4) and set
it in the request headers (e.g., headers["Idempotency-Key"] or
"Idempotency-Token") before calling post_with_retry for
STEADWING_ANALYZE_ENDPOINT with payload and STEADWING_REQUEST_TIMEOUT so retries
are deduped, or alternatively call the non-retrying POST helper for this path if
the Steadwing API does not support idempotency; modify the call site that
currently uses post_with_retry(headers=headers, json=payload, ...) to include
the idempotency header (or swap to the no-retry helper).

try:
body = resp.json()
except ValueError as exc:
# Malformed / non-JSON body. Log the status only — never the body,
# which can echo the submitted error context — and re-raise.
status = getattr(resp, 'status_code', None)
warning(f'Steadwing API returned a non-JSON response body: status={status}')
raise RuntimeError('Steadwing returned a non-JSON response body') from exc

if not isinstance(body, dict):
raise RuntimeError(f'Steadwing returned an unexpected payload type: {type(body).__name__}')

api_error = body.get('error')
if isinstance(api_error, dict):
msg = api_error.get('message') or api_error.get('detail') or api_error.get('code') or 'unknown error'
raise RuntimeError(f'Steadwing API error: {msg}')
if api_error:
raise RuntimeError(f'Steadwing API error: {api_error}')

return _shape_result(body)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _normalize_files(files: Any) -> List[Dict[str, str]]:
"""Coerce the optional ``files`` argument into a clean ``[{name, content}]`` list (max 20)."""
if not files:
return []
if not isinstance(files, list):
raise ValueError('files must be an array of {name, content} objects')

out: List[Dict[str, str]] = []
for item in files[:MAX_FILES]:
if not isinstance(item, dict):
continue
name = str(item.get('name') or '').strip()
content = item.get('content')
if not name or not isinstance(content, str):
continue
out.append({'name': name, 'content': content})
return out


def _shape_result(body: Dict[str, Any]) -> Dict[str, Any]:
"""Extract the Steadwing investigation URL from an ``analyze`` response.

The endpoint returns the URL either at the root or nested under ``data``
(``{"data": {"incident_url": ...}}``); accept either, plus a few key spellings.
"""
data = body.get('data') if isinstance(body.get('data'), dict) else {}
incident_url = (
data.get('incident_url')
or data.get('incidentUrl')
or data.get('url')
or body.get('incident_url')
or body.get('incidentUrl')
or body.get('url')
or ''
)
incident_url = str(incident_url or '').strip()
if not incident_url:
raise RuntimeError('Steadwing response did not include an investigation URL')

return {
'success': True,
'incident_url': incident_url,
'message': f'Root-cause analysis started. Track the investigation at {incident_url}',
}
29 changes: 29 additions & 0 deletions nodes/src/nodes/tool_steadwing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

from .IGlobal import IGlobal
from .IInstance import IInstance

__all__ = ['IGlobal', 'IInstance']
74 changes: 74 additions & 0 deletions nodes/src/nodes/tool_steadwing/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# tool_steadwing

Exposes [Steadwing](https://www.steadwing.com)'s AI root-cause analysis as an agent tool node.

## What it does

Agents invoke this node via the tool invoke channel. Given a production error or stack
trace (and, optionally, the relevant source files), `run_rca` opens a Steadwing
investigation that correlates logs, metrics, traces, and code across the stack, and
returns the investigation URL.

Because `lanes` is empty (`{}`), this node has no pipeline input/output lanes — it is
consumed exclusively by agent runtimes through the `invoke` capability.

`run_rca` is **asynchronous**: it returns the investigation URL immediately and the
analysis runs in the background on the Steadwing platform. Call it once per incident —
the URL is the deliverable.

## Setup

Set your Steadwing API key (from `app.steadwing.com/organization`) via the node config
field **API Key** or the environment variable:

```bash
ROCKETRIDE_STEADWING_KEY=st_...
```

## Tool — `run_rca`

| Parameter | Required | Description |
| --------- | -------- | ----------------------------------------------------------------------------------- |
| `error` | yes | The full error message, stack trace, or incident description to analyze. |
| `files` | no | Up to 20 source files (`{ name, content }`) for context — start with the file named in the stack trace. |

Returns `{ success, incident_url, message }`. Backed by
`POST https://api.steadwing.com/api/mcp/analyze` (`X-API-Key`).

## Config fields

| Field | Default | Description |
| ------- | --------- | --------------------------------------------------------------------------- |
| API Key | *(empty)* | Steadwing API key. Env fallback `ROCKETRIDE_STEADWING_KEY`. Encrypted at rest. |

## Status — V0

Intentional first cut: a minimal `run_rca` wrapper. Planned next (notably for power
users): return the finished analysis (not just the tracking URL); richer `files`
ergonomics; idempotency / guardrails so agent loops don't burn metered RCAs; a
Steadwing-incident → pipeline trigger node for autonomous on-call; power-user config
(analysis depth, integrations, output shaping).

## Reference

<!-- ROCKETRIDE:GENERATED:PARAMS START -->
<!-- Generated by nodes:docs-generate. Do not edit by hand. -->

| Property | Value |
| --- | --- |
| Class type | tool |
| Capabilities | invoke, experimental |
| Protocol | `tool_steadwing://` |

**Profiles**

| Profile | Title | Model |
| --- | --- | --- |
| `default` | Steadwing RCA | |

**Configuration sections**

| Section | Fields |
| --- | --- |
| Steadwing RCA | `type`, `tool_steadwing.apikey` |
<!-- ROCKETRIDE:GENERATED:PARAMS END -->
Loading
Loading