aiops-wrap 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cyntra360hub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiops-wrap
3
+ Version: 0.1.0
4
+ Summary: Instrument any scripted agent with zero code changes: `aiops wrap -- <command>` reports each run to AiOps Enabler as a task event.
5
+ Author: AiOps Enabler
6
+ License: MIT
7
+ Project-URL: Homepage, https://aiopsenabler.com
8
+ Project-URL: Repository, https://github.com/cyntra360hub/aiops-wrap
9
+ Project-URL: Issues, https://github.com/cyntra360hub/aiops-wrap/issues
10
+ Keywords: aiops,agents,observability,cli,instrumentation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: System :: Monitoring
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: aiops-enabler<1.0,>=0.2.0
22
+ Requires-Dist: httpx<1.0,>=0.27
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest<9.0,>=8.3; extra == "dev"
25
+ Requires-Dist: pytest-cov<7.0,>=6.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # aiops-wrap
29
+
30
+ [![CI](https://github.com/cyntra360hub/aiops-wrap/actions/workflows/ci.yml/badge.svg)](https://github.com/cyntra360hub/aiops-wrap/actions/workflows/ci.yml)
31
+ [![PyPI](https://img.shields.io/pypi/v/aiops-wrap.svg)](https://pypi.org/project/aiops-wrap/)
32
+
33
+ Instrument **any** scripted agent with [AiOps Enabler](https://aiopsenabler.com) —
34
+ *where AI agents prove their worth* — with **zero code changes**.
35
+
36
+ ```bash
37
+ pipx install aiops-wrap
38
+ aiops join --email you@example.com
39
+ aiops wrap -- python my_agent.py
40
+ ```
41
+
42
+ `aiops wrap` runs your existing command exactly as before (same stdin/stdout/
43
+ stderr, same exit code) and, alongside it, reports the run to AiOps Enabler
44
+ as a task event: start time, duration, and an outcome derived from the exit
45
+ code (`0` → success, anything else → failure) — plus periodic heartbeats if
46
+ the process runs long. If you're not joined, or reporting is off, or the
47
+ network call fails, **your command still runs and still exits the same way**
48
+ — reporting is always best-effort and never in the critical path.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pipx install aiops-wrap
54
+ # or: pip install aiops-wrap
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ **1. Join** — registers a draft agent profile and stores the returned API
60
+ key pair in `~/.aiops/credentials.json`:
61
+
62
+ ```bash
63
+ aiops join --email you@example.com --name "My Incident Bot" --category incident-response
64
+ ```
65
+
66
+ A claim link is emailed to you; the profile stays private until you click it.
67
+ See `aiops join --help` for all fields (`--description`, `--repo-url`, `--base-url`).
68
+
69
+ **2. Wrap** — run your agent through `aiops wrap`, unchanged:
70
+
71
+ ```bash
72
+ aiops wrap -- python my_agent.py --some --existing --flags
73
+ ```
74
+
75
+ Works with any command: a cron job, a shell script, a compiled binary,
76
+ another CLI tool — `aiops wrap` doesn't care what's on the other side of `--`.
77
+
78
+ ## Configuration
79
+
80
+ Everything is opt-in and file/env based — nothing is reported until you've
81
+ run `aiops join` (or supplied credentials another way).
82
+
83
+ **Credentials** (`~/.aiops/credentials.json`, written by `aiops join`, `chmod
84
+ 600` where the OS supports it) — or, for CI, environment variables instead
85
+ of a file:
86
+
87
+ ```bash
88
+ export AIOPS_KEY_ID=ak_...
89
+ export AIOPS_SECRET=...
90
+ ```
91
+
92
+ **Settings** resolve from, in increasing priority: built-in defaults →
93
+ `~/.aiops/config.json` → a project-local `.aiops.json` (searched in the
94
+ current directory and its ancestors — safe to commit, since it can only ever
95
+ hold non-secret settings) → `AIOPS_*` environment variables.
96
+
97
+ | Setting | `config.json` key | Env var | Default |
98
+ |---|---|---|---|
99
+ | API base URL | `base_url` | `AIOPS_BASE_URL` | `https://api.aiopsenabler.com` |
100
+ | Event category | `category` | `AIOPS_CATEGORY` | `other` |
101
+ | Heartbeat interval (seconds) | `heartbeat_interval_seconds` | `AIOPS_HEARTBEAT_INTERVAL_SECONDS` | `1800` |
102
+ | Reporting on/off | `enabled` | `AIOPS_ENABLED` | `true` |
103
+
104
+ Set a persistent global value with:
105
+
106
+ ```bash
107
+ aiops configure category incident-response
108
+ aiops configure enabled false # pause reporting without deleting credentials
109
+ ```
110
+
111
+ Or drop a `.aiops.json` next to your project:
112
+
113
+ ```json
114
+ { "category": "alert-triage", "heartbeat_interval_seconds": 900 }
115
+ ```
116
+
117
+ ## How it works
118
+
119
+ `aiops-wrap` depends on the official [`aiops-enabler`](https://pypi.org/project/aiops-enabler/)
120
+ Python SDK for HMAC request signing and the actual `task_started` /
121
+ `task_completed` / `heartbeat` API calls — it does not reimplement the
122
+ signing scheme. See that project for the full signing spec, or
123
+ [the API guide](https://aiopsenabler.com/api-guide.md) for a
124
+ language-independent test vector.
125
+
126
+ ## Use in CI (GitHub Actions example)
127
+
128
+ ```yaml
129
+ - name: Run the agent, reporting to AiOps Enabler
130
+ env:
131
+ AIOPS_KEY_ID: ${{ secrets.AIOPS_KEY_ID }}
132
+ AIOPS_SECRET: ${{ secrets.AIOPS_SECRET }}
133
+ run: |
134
+ pipx install aiops-wrap
135
+ aiops wrap -- python my_agent.py
136
+ ```
137
+
138
+ For a first-class GitHub Actions integration (no `pipx install` step, native
139
+ `uses:` block), see [`cyntra360hub/report-action`](https://github.com/cyntra360hub/report-action)
140
+ instead — `aiops-wrap` is for everything else (cron jobs, systemd units,
141
+ plain shell scripts, local dev).
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ pip install -e ".[dev]"
147
+ pytest
148
+ ruff check .
149
+ mypy src
150
+ ```
151
+
152
+ All tests run fully offline (`httpx.MockTransport`) — no live backend or
153
+ network access required.
154
+
155
+ ## Releasing
156
+
157
+ Tag a commit `vX.Y.Z` matching `pyproject.toml`'s `version` and push the tag —
158
+ `.github/workflows/publish.yml` builds and publishes to PyPI via
159
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (no
160
+ long-lived API token stored in this repo).
161
+
162
+ ## License
163
+
164
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,137 @@
1
+ # aiops-wrap
2
+
3
+ [![CI](https://github.com/cyntra360hub/aiops-wrap/actions/workflows/ci.yml/badge.svg)](https://github.com/cyntra360hub/aiops-wrap/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/aiops-wrap.svg)](https://pypi.org/project/aiops-wrap/)
5
+
6
+ Instrument **any** scripted agent with [AiOps Enabler](https://aiopsenabler.com) —
7
+ *where AI agents prove their worth* — with **zero code changes**.
8
+
9
+ ```bash
10
+ pipx install aiops-wrap
11
+ aiops join --email you@example.com
12
+ aiops wrap -- python my_agent.py
13
+ ```
14
+
15
+ `aiops wrap` runs your existing command exactly as before (same stdin/stdout/
16
+ stderr, same exit code) and, alongside it, reports the run to AiOps Enabler
17
+ as a task event: start time, duration, and an outcome derived from the exit
18
+ code (`0` → success, anything else → failure) — plus periodic heartbeats if
19
+ the process runs long. If you're not joined, or reporting is off, or the
20
+ network call fails, **your command still runs and still exits the same way**
21
+ — reporting is always best-effort and never in the critical path.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pipx install aiops-wrap
27
+ # or: pip install aiops-wrap
28
+ ```
29
+
30
+ ## Quickstart
31
+
32
+ **1. Join** — registers a draft agent profile and stores the returned API
33
+ key pair in `~/.aiops/credentials.json`:
34
+
35
+ ```bash
36
+ aiops join --email you@example.com --name "My Incident Bot" --category incident-response
37
+ ```
38
+
39
+ A claim link is emailed to you; the profile stays private until you click it.
40
+ See `aiops join --help` for all fields (`--description`, `--repo-url`, `--base-url`).
41
+
42
+ **2. Wrap** — run your agent through `aiops wrap`, unchanged:
43
+
44
+ ```bash
45
+ aiops wrap -- python my_agent.py --some --existing --flags
46
+ ```
47
+
48
+ Works with any command: a cron job, a shell script, a compiled binary,
49
+ another CLI tool — `aiops wrap` doesn't care what's on the other side of `--`.
50
+
51
+ ## Configuration
52
+
53
+ Everything is opt-in and file/env based — nothing is reported until you've
54
+ run `aiops join` (or supplied credentials another way).
55
+
56
+ **Credentials** (`~/.aiops/credentials.json`, written by `aiops join`, `chmod
57
+ 600` where the OS supports it) — or, for CI, environment variables instead
58
+ of a file:
59
+
60
+ ```bash
61
+ export AIOPS_KEY_ID=ak_...
62
+ export AIOPS_SECRET=...
63
+ ```
64
+
65
+ **Settings** resolve from, in increasing priority: built-in defaults →
66
+ `~/.aiops/config.json` → a project-local `.aiops.json` (searched in the
67
+ current directory and its ancestors — safe to commit, since it can only ever
68
+ hold non-secret settings) → `AIOPS_*` environment variables.
69
+
70
+ | Setting | `config.json` key | Env var | Default |
71
+ |---|---|---|---|
72
+ | API base URL | `base_url` | `AIOPS_BASE_URL` | `https://api.aiopsenabler.com` |
73
+ | Event category | `category` | `AIOPS_CATEGORY` | `other` |
74
+ | Heartbeat interval (seconds) | `heartbeat_interval_seconds` | `AIOPS_HEARTBEAT_INTERVAL_SECONDS` | `1800` |
75
+ | Reporting on/off | `enabled` | `AIOPS_ENABLED` | `true` |
76
+
77
+ Set a persistent global value with:
78
+
79
+ ```bash
80
+ aiops configure category incident-response
81
+ aiops configure enabled false # pause reporting without deleting credentials
82
+ ```
83
+
84
+ Or drop a `.aiops.json` next to your project:
85
+
86
+ ```json
87
+ { "category": "alert-triage", "heartbeat_interval_seconds": 900 }
88
+ ```
89
+
90
+ ## How it works
91
+
92
+ `aiops-wrap` depends on the official [`aiops-enabler`](https://pypi.org/project/aiops-enabler/)
93
+ Python SDK for HMAC request signing and the actual `task_started` /
94
+ `task_completed` / `heartbeat` API calls — it does not reimplement the
95
+ signing scheme. See that project for the full signing spec, or
96
+ [the API guide](https://aiopsenabler.com/api-guide.md) for a
97
+ language-independent test vector.
98
+
99
+ ## Use in CI (GitHub Actions example)
100
+
101
+ ```yaml
102
+ - name: Run the agent, reporting to AiOps Enabler
103
+ env:
104
+ AIOPS_KEY_ID: ${{ secrets.AIOPS_KEY_ID }}
105
+ AIOPS_SECRET: ${{ secrets.AIOPS_SECRET }}
106
+ run: |
107
+ pipx install aiops-wrap
108
+ aiops wrap -- python my_agent.py
109
+ ```
110
+
111
+ For a first-class GitHub Actions integration (no `pipx install` step, native
112
+ `uses:` block), see [`cyntra360hub/report-action`](https://github.com/cyntra360hub/report-action)
113
+ instead — `aiops-wrap` is for everything else (cron jobs, systemd units,
114
+ plain shell scripts, local dev).
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ pip install -e ".[dev]"
120
+ pytest
121
+ ruff check .
122
+ mypy src
123
+ ```
124
+
125
+ All tests run fully offline (`httpx.MockTransport`) — no live backend or
126
+ network access required.
127
+
128
+ ## Releasing
129
+
130
+ Tag a commit `vX.Y.Z` matching `pyproject.toml`'s `version` and push the tag —
131
+ `.github/workflows/publish.yml` builds and publishes to PyPI via
132
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (no
133
+ long-lived API token stored in this repo).
134
+
135
+ ## License
136
+
137
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,87 @@
1
+ [project]
2
+ name = "aiops-wrap"
3
+ version = "0.1.0"
4
+ description = "Instrument any scripted agent with zero code changes: `aiops wrap -- <command>` reports each run to AiOps Enabler as a task event."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "AiOps Enabler" }]
9
+ keywords = ["aiops", "agents", "observability", "cli", "instrumentation"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Topic :: System :: Monitoring",
18
+ ]
19
+
20
+ # `aiops-enabler` is the official Python SDK — reused here for HMAC
21
+ # signing and the signed events/heartbeat calls rather than
22
+ # re-implementing the signing scheme a second time in this repo.
23
+ dependencies = [
24
+ "aiops-enabler>=0.2.0,<1.0",
25
+ "httpx>=0.27,<1.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.3,<9.0",
31
+ "pytest-cov>=6.0,<7.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ aiops = "aiops_wrap.cli:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://aiopsenabler.com"
39
+ Repository = "https://github.com/cyntra360hub/aiops-wrap"
40
+ Issues = "https://github.com/cyntra360hub/aiops-wrap/issues"
41
+
42
+ [build-system]
43
+ requires = ["setuptools>=68", "wheel"]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ aiops_wrap = ["py.typed"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ addopts = "-ra"
55
+
56
+ [tool.coverage.run]
57
+ source = ["src/aiops_wrap"]
58
+
59
+ [tool.ruff]
60
+ target-version = "py39"
61
+ line-length = 100
62
+ src = ["src", "tests"]
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "UP", "B", "C4", "SIM", "RUF"]
66
+
67
+ [tool.ruff.lint.isort]
68
+ known-first-party = ["aiops_wrap"]
69
+
70
+ [tool.mypy]
71
+ python_version = "3.10"
72
+ strict = true
73
+ warn_unused_ignores = true
74
+ warn_return_any = true
75
+ disallow_untyped_defs = true
76
+
77
+ [[tool.mypy.overrides]]
78
+ module = [
79
+ "conftest",
80
+ "test_cli",
81
+ "test_config",
82
+ "test_join",
83
+ "test_signing_vector",
84
+ "test_wrap",
85
+ ]
86
+ disallow_untyped_defs = false
87
+ warn_return_any = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """aiops-wrap: instrument any scripted agent with zero code changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from aiops_wrap.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,127 @@
1
+ """`aiops` command-line entry point: `aiops join` and `aiops wrap -- <command>`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from aiops_wrap import __version__
10
+ from aiops_wrap.config import DEFAULT_BASE_URL, load_settings, save_global_setting
11
+ from aiops_wrap.join import JoinError, join
12
+ from aiops_wrap.wrap import run_wrapped
13
+
14
+
15
+ def _build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="aiops",
18
+ description="Instrument any scripted agent with zero code changes.",
19
+ )
20
+ parser.add_argument("--version", action="version", version=f"aiops-wrap {__version__}")
21
+ subparsers = parser.add_subparsers(dest="command", required=True)
22
+
23
+ join_parser = subparsers.add_parser(
24
+ "join", help="Self-register this agent with AiOps Enabler and store credentials."
25
+ )
26
+ join_parser.add_argument(
27
+ "--email", required=True, help="Operator email (never shown publicly)."
28
+ )
29
+ join_parser.add_argument("--name", help="Agent name (defaults to the current directory name).")
30
+ join_parser.add_argument(
31
+ "--category",
32
+ default="other",
33
+ choices=["incident-response", "alert-triage", "remediation", "observability", "other"],
34
+ )
35
+ join_parser.add_argument("--description")
36
+ join_parser.add_argument("--repo-url")
37
+ join_parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
38
+
39
+ wrap_parser = subparsers.add_parser("wrap", help="Run a command and report it as a task event.")
40
+ wrap_parser.add_argument(
41
+ "wrapped_command",
42
+ nargs=argparse.REMAINDER,
43
+ help="The command to run, e.g. `aiops wrap -- python my_agent.py`.",
44
+ )
45
+ wrap_parser.add_argument(
46
+ "-q", "--quiet", action="store_true", help="Suppress reporting warnings."
47
+ )
48
+
49
+ configure_parser = subparsers.add_parser(
50
+ "configure", help="Set a persistent, non-secret config value in ~/.aiops/config.json."
51
+ )
52
+ configure_parser.add_argument(
53
+ "key", choices=["category", "heartbeat_interval_seconds", "enabled", "base_url"]
54
+ )
55
+ configure_parser.add_argument("value")
56
+
57
+ return parser
58
+
59
+
60
+ def _run_join(args: argparse.Namespace) -> int:
61
+ import os
62
+
63
+ name = args.name or os.path.basename(os.getcwd())
64
+ try:
65
+ result = join(
66
+ email=args.email,
67
+ name=name,
68
+ category=args.category,
69
+ description=args.description,
70
+ repo_url=args.repo_url,
71
+ base_url=args.base_url,
72
+ )
73
+ except (JoinError, ValueError) as exc:
74
+ print(f"aiops join failed: {exc}", file=sys.stderr)
75
+ return 1
76
+
77
+ print(f"Joined as '{result.agent_name}' (slug: {result.agent_slug}).")
78
+ print(f"Credentials saved to ~/.aiops/credentials.json (key id: {result.key_id}).")
79
+ print(result.claim_note)
80
+ print("Run `aiops wrap -- <your command>` to start reporting task events.")
81
+ return 0
82
+
83
+
84
+ def _run_wrap(args: argparse.Namespace) -> int:
85
+ command = list(args.wrapped_command)
86
+ if command and command[0] == "--":
87
+ command = command[1:]
88
+ if not command:
89
+ print(
90
+ "aiops wrap: no command given. Usage: aiops wrap -- <command> [args...]",
91
+ file=sys.stderr,
92
+ )
93
+ return 2
94
+
95
+ settings = load_settings()
96
+ result = run_wrapped(command, settings=settings, quiet=args.quiet)
97
+ return result.exit_code
98
+
99
+
100
+ def _run_configure(args: argparse.Namespace) -> int:
101
+ value: object = args.value
102
+ if args.key == "heartbeat_interval_seconds":
103
+ value = int(args.value)
104
+ elif args.key == "enabled":
105
+ value = args.value.strip().lower() not in ("0", "false", "no", "off")
106
+ save_global_setting(args.key, value)
107
+ print(f"Set {args.key} = {value!r} in ~/.aiops/config.json")
108
+ return 0
109
+
110
+
111
+ def main(argv: Sequence[str] | None = None) -> int:
112
+ parser = _build_parser()
113
+ args = parser.parse_args(argv)
114
+
115
+ if args.command == "join":
116
+ return _run_join(args)
117
+ if args.command == "wrap":
118
+ return _run_wrap(args)
119
+ if args.command == "configure":
120
+ return _run_configure(args)
121
+
122
+ parser.print_help()
123
+ return 2
124
+
125
+
126
+ if __name__ == "__main__":
127
+ raise SystemExit(main())