anosys-codex 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.
- anosys_codex-0.1.0.dist-info/METADATA +142 -0
- anosys_codex-0.1.0.dist-info/RECORD +13 -0
- anosys_codex-0.1.0.dist-info/WHEEL +5 -0
- anosys_codex-0.1.0.dist-info/entry_points.txt +2 -0
- anosys_codex-0.1.0.dist-info/licenses/LICENSE +1 -0
- anosys_codex-0.1.0.dist-info/top_level.txt +1 -0
- anosys_sdk_codex/__init__.py +40 -0
- anosys_sdk_codex/cli.py +200 -0
- anosys_sdk_codex/encryption.py +182 -0
- anosys_sdk_codex/hook_runner.py +607 -0
- anosys_sdk_codex/installer.py +287 -0
- anosys_sdk_codex/mapper.py +278 -0
- anosys_sdk_codex/py.typed +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anosys-codex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AnoSys observability hook for OpenAI Codex CLI
|
|
5
|
+
Author-email: AnoSys <support@anosys.ai>
|
|
6
|
+
License: ../../../LICENSE
|
|
7
|
+
Project-URL: Homepage, https://anosys.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.anosys.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/anosys-ai/anosys-sdk
|
|
10
|
+
Keywords: anosys,codex,openai,observability,monitoring,audit,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.25.1
|
|
25
|
+
Requires-Dist: cryptography>=3.4.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# anosys-codex (Python)
|
|
29
|
+
|
|
30
|
+
AnoSys observability and analytics hook for OpenAI Codex CLI.
|
|
31
|
+
|
|
32
|
+
This package automatically captures Codex CLI's telemetry, agent turns, tool calls (`shell`, `apply_patch`, Code Mode custom tools, `web_search`), token usage breakdowns (prompt, completion, cache read/write, reasoning tokens), and estimated API costs, sending them directly to your AnoSys workspace.
|
|
33
|
+
|
|
34
|
+
It integrates seamlessly with Codex CLI's native `notify` hook mechanism.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install anosys-codex
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After installing the package, run the setup wizard to configure the hook:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
anosys-codex install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The installer will prompt you for:
|
|
51
|
+
1. **AnoSys API Key for logs**: Your ingestion API key from the [AnoSys Console](https://console.anosys.ai/collect/integrationoptions).
|
|
52
|
+
> **Pixel Type Requirement**: This key must belong to a **`CC` (Claude Code / Codex CLI)** pixel. The installer automatically validates the key against `https://console.anosys.ai/api/resolveapikeys` to ensure it resolves to a `/cc/` endpoint. Keys belonging to `T` (OTEL) or Web Traffic (`a`/`g`) pixels are rejected with a warning.
|
|
53
|
+
2. **Content Redaction**: If enabled, sensitive conversation content (prompts, answers, code, tool inputs/outputs) will be redacted and replaced with `[REDACTED]`. Only metadata and token statistics will be tracked.
|
|
54
|
+
3. **Automatic Config Update**: Confirms whether to automatically write `notify = ["anosys-codex", "run"]` to `~/.codex/config.toml` (and creates a `.bak` backup first).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Non-Interactive / Headless Installation
|
|
59
|
+
|
|
60
|
+
For CI/CD pipelines, Docker containers, devcontainer setups, or non-interactive dotfiles:
|
|
61
|
+
|
|
62
|
+
### Using the `-y` / `--yes` flag (Recommended):
|
|
63
|
+
```bash
|
|
64
|
+
anosys-codex install --api-key "your_logs_api_key" -y
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Using environment variables:
|
|
68
|
+
```bash
|
|
69
|
+
export ANOSYS_API_KEY="your_logs_api_key"
|
|
70
|
+
anosys-codex install -y
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Using explicit granular flags:
|
|
74
|
+
```bash
|
|
75
|
+
anosys-codex install \
|
|
76
|
+
--api-key "your_logs_api_key" \
|
|
77
|
+
--auto-update \
|
|
78
|
+
--no-redaction
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> **Important (Codex Security Trust)**:
|
|
82
|
+
> Codex requires explicit user approval for unmanaged external notify hooks. After running `install`, start a Codex CLI session, type `/hooks`, and select `approve` / `trust` for the `anosys-codex run` hook entry.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## CLI Commands
|
|
87
|
+
|
|
88
|
+
The CLI safely manages `~/.codex/config.toml` and creates automatic timestamped backups (`config.toml.<TIMESTAMP>.bak`) before applying any modifications.
|
|
89
|
+
|
|
90
|
+
### `status`
|
|
91
|
+
Check the current installation and registration status of the hook:
|
|
92
|
+
```bash
|
|
93
|
+
anosys-codex status
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `install`
|
|
97
|
+
Installs or updates the notify hook in `~/.codex/config.toml` and creates `~/.codex/anosys-env.sh`:
|
|
98
|
+
```bash
|
|
99
|
+
anosys-codex install [options]
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--api-key <key> AnoSys logs API key
|
|
103
|
+
-y, --yes Skip interactive prompts (non-interactive mode)
|
|
104
|
+
--redaction Enable content redaction
|
|
105
|
+
--no-redaction Disable content redaction
|
|
106
|
+
--auto-update Automatically update ~/.codex/config.toml
|
|
107
|
+
--no-auto-update Print manual setup instructions instead of modifying config
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `uninstall`
|
|
111
|
+
Safely removes the AnoSys notify hook entry from `~/.codex/config.toml` and deletes `~/.codex/anosys-env.sh`:
|
|
112
|
+
```bash
|
|
113
|
+
anosys-codex uninstall
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `run`
|
|
117
|
+
Executed automatically by the Codex CLI on turn-complete notify events. You do not need to invoke this manually:
|
|
118
|
+
```bash
|
|
119
|
+
anosys-codex run
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How It Works
|
|
125
|
+
|
|
126
|
+
1. When a Codex turn completes, Codex CLI triggers `notify = ["anosys-codex", "run"]` defined in `~/.codex/config.toml`.
|
|
127
|
+
2. The hook runner receives the session thread ID and locates the session's rollout transcript at `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<session_id>.jsonl`.
|
|
128
|
+
3. It incrementally extracts user prompts, assistant messages, model details, token counts (including cached inputs and reasoning tokens), and tool calls.
|
|
129
|
+
4. It maps the turn into the AnoSys schema (`cvs200: 'CodexHook'`), estimates USD cost using OpenAI's model pricing table, applies optional PII redaction, and batches the payload to `https://api.anosys.ai/ingestion`.
|
|
130
|
+
5. If network connectivity is temporarily unavailable, failed records are buffered locally in `~/.codex/state/pending_records.jsonl` and automatically replayed on subsequent runs.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Environment Variables
|
|
135
|
+
|
|
136
|
+
| Variable | Description |
|
|
137
|
+
|----------|-------------|
|
|
138
|
+
| `ANOSYS_HOOK_APIKEY` / `ANOSYS_API_KEY` | AnoSys ingestion API key |
|
|
139
|
+
| `ANOSYS_HOOK_ENDPOINT_URL` | Override default ingestion URL (`https://api.anosys.ai/ingestion`) |
|
|
140
|
+
| `REDACTION` / `ANOSYS_REDACT_CONTENT` | Set to `"true"` to redact prompts, code, and tool outputs |
|
|
141
|
+
| `ANOSYS_HOOK_DRY_RUN` | Set to `"true"` to log payloads to stderr without sending to network |
|
|
142
|
+
| `CODEX_HOME` | Custom directory for Codex config and sessions (defaults to `~/.codex`) |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
anosys_codex-0.1.0.dist-info/licenses/LICENSE,sha256=rZsldTRUIZ33Yi94i4v6n7ffaPNNQmzB6gxnWzGlf2A,16
|
|
2
|
+
anosys_sdk_codex/__init__.py,sha256=6iIPOqOJdN2wOzvLzeJBdsopL6yn7a4swZgYeKYAT_s,995
|
|
3
|
+
anosys_sdk_codex/cli.py,sha256=GVfkaujy64cR2KA6zvV6f8Edquh4G8tChlyuNhBjj9E,6972
|
|
4
|
+
anosys_sdk_codex/encryption.py,sha256=c-zJUge2V1SZe00nsD6Zjt29IzO49g3vEYBB0p_7evQ,5817
|
|
5
|
+
anosys_sdk_codex/hook_runner.py,sha256=qi_BhfytXQ2668ohDUwyYpnwtsDYpIINiQDITCa1tz4,22484
|
|
6
|
+
anosys_sdk_codex/installer.py,sha256=Kv-JRc1rNyBiHhlbXHbhS023idi14vX27WTNeFBAQrA,8742
|
|
7
|
+
anosys_sdk_codex/mapper.py,sha256=jg87lUW3IprkwLYe2VC-xoKyXEO0e2Fc3f55r0vmM-o,9355
|
|
8
|
+
anosys_sdk_codex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
anosys_codex-0.1.0.dist-info/METADATA,sha256=srGxLyocRL27cGFFV6dskSE044Vg3FP8dk0XKXWQaP0,5965
|
|
10
|
+
anosys_codex-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
11
|
+
anosys_codex-0.1.0.dist-info/entry_points.txt,sha256=G-RhC_Pcgrg64y4ebUCqD7TbJfzTk_piyJKpugAgfeY,59
|
|
12
|
+
anosys_codex-0.1.0.dist-info/top_level.txt,sha256=TnOBiZGoAsifFOyUXKafdwHZ6we-8jwso09VrUbwmYI,17
|
|
13
|
+
anosys_codex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../../LICENSE
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
anosys_sdk_codex
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AnoSys OpenAI Codex CLI integration — observability hook for Codex sessions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
except ImportError:
|
|
8
|
+
from importlib_metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
from anosys_sdk_codex.installer import update_codex_config, update_codex_env
|
|
11
|
+
from anosys_sdk_codex.mapper import transform_codex_turn
|
|
12
|
+
from anosys_sdk_codex.encryption import (
|
|
13
|
+
ANOSYS_ENCRYPTION_PREFIX,
|
|
14
|
+
DEFAULT_ENCRYPTED_COLUMNS,
|
|
15
|
+
derive_256_bit_key,
|
|
16
|
+
encrypt,
|
|
17
|
+
decrypt,
|
|
18
|
+
get_encryption_key,
|
|
19
|
+
get_target_columns,
|
|
20
|
+
encrypt_payload,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = version("anosys-codex")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"transform_codex_turn",
|
|
30
|
+
"update_codex_config",
|
|
31
|
+
"update_codex_env",
|
|
32
|
+
"ANOSYS_ENCRYPTION_PREFIX",
|
|
33
|
+
"DEFAULT_ENCRYPTED_COLUMNS",
|
|
34
|
+
"derive_256_bit_key",
|
|
35
|
+
"encrypt",
|
|
36
|
+
"decrypt",
|
|
37
|
+
"get_encryption_key",
|
|
38
|
+
"get_target_columns",
|
|
39
|
+
"encrypt_payload",
|
|
40
|
+
]
|
anosys_sdk_codex/cli.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for anosys-codex.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
install Register the AnoSys notify hook in ~/.codex/config.toml
|
|
6
|
+
uninstall Remove the AnoSys notify hook from ~/.codex/config.toml
|
|
7
|
+
status Show current hook registration status
|
|
8
|
+
run Execute the hook (invoked by Codex CLI on notify events)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from anosys_sdk_codex.installer import (
|
|
18
|
+
HOOK_COMMAND,
|
|
19
|
+
INGESTION_URL,
|
|
20
|
+
backup,
|
|
21
|
+
get_config_path,
|
|
22
|
+
get_env_path,
|
|
23
|
+
has_anosys_hook,
|
|
24
|
+
load_toml,
|
|
25
|
+
remove_codex_config,
|
|
26
|
+
remove_codex_env,
|
|
27
|
+
update_codex_config,
|
|
28
|
+
update_codex_env,
|
|
29
|
+
validate_api_key,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _prompt(prompt: str, default: str = "") -> str:
|
|
34
|
+
try:
|
|
35
|
+
val = input(prompt).strip()
|
|
36
|
+
return val if val else default
|
|
37
|
+
except (EOFError, KeyboardInterrupt):
|
|
38
|
+
print()
|
|
39
|
+
sys.exit(0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_install(args: argparse.Namespace) -> None:
|
|
43
|
+
print("\nAnoSys OpenAI Codex Hook Installer")
|
|
44
|
+
print("=" * 45)
|
|
45
|
+
|
|
46
|
+
yes = getattr(args, "yes", False)
|
|
47
|
+
|
|
48
|
+
redaction = args.redaction
|
|
49
|
+
if not redaction and not args.no_redaction:
|
|
50
|
+
if yes:
|
|
51
|
+
redaction = False
|
|
52
|
+
else:
|
|
53
|
+
choice = _prompt("Enable content redaction? (y/N): ", "n").lower()
|
|
54
|
+
redaction = choice == "y"
|
|
55
|
+
|
|
56
|
+
api_key = args.api_key or os.environ.get("ANOSYS_API_KEY") or os.environ.get("ANOSYS_HOOK_APIKEY") or ""
|
|
57
|
+
if not api_key and not yes:
|
|
58
|
+
api_key = _prompt("AnoSys API key for logs (leave blank to skip): ")
|
|
59
|
+
|
|
60
|
+
if api_key:
|
|
61
|
+
print("Validating Logs API key...")
|
|
62
|
+
if not validate_api_key(api_key, "cc"):
|
|
63
|
+
print("⚠️ Warning: Logs API key validation failed: key does not belong to a CC (Claude Code / Codex) pixel or is invalid.")
|
|
64
|
+
else:
|
|
65
|
+
print("✅ Logs API key is valid (CC pixel).")
|
|
66
|
+
|
|
67
|
+
auto_update = args.auto_update
|
|
68
|
+
if auto_update is None:
|
|
69
|
+
if yes:
|
|
70
|
+
auto_update = True
|
|
71
|
+
else:
|
|
72
|
+
choice = _prompt("Would you like to automatically update ~/.codex/config.toml? (Y/n): ", "y").lower()
|
|
73
|
+
auto_update = choice != "n"
|
|
74
|
+
|
|
75
|
+
config_path = get_config_path()
|
|
76
|
+
env_path = get_env_path()
|
|
77
|
+
|
|
78
|
+
if auto_update:
|
|
79
|
+
print(f"\nUpdating {config_path} ...")
|
|
80
|
+
backup_path = backup(config_path)
|
|
81
|
+
update_codex_config(HOOK_COMMAND, path=config_path)
|
|
82
|
+
update_codex_env(api_key=api_key, redaction=redaction, path=env_path)
|
|
83
|
+
|
|
84
|
+
if backup_path:
|
|
85
|
+
print(f" Backed up original settings -> {backup_path}")
|
|
86
|
+
print(' Hook command registered in notify: ["anosys-codex", "run"]')
|
|
87
|
+
print(f" Ingestion URL: {INGESTION_URL}")
|
|
88
|
+
if api_key:
|
|
89
|
+
masked = ("*" * (len(api_key) - 4)) + api_key[-4:] if len(api_key) > 4 else "****"
|
|
90
|
+
print(f" Logs API key: {masked}")
|
|
91
|
+
print(f" Redaction: {'enabled' if redaction else 'disabled'}")
|
|
92
|
+
print("\n✅ Codex hook installed successfully.")
|
|
93
|
+
print("\n⚠️ IMPORTANT (Codex Security Trust):")
|
|
94
|
+
print(" In your next Codex CLI session, run the '/hooks' command")
|
|
95
|
+
print(f" and approve the '{HOOK_COMMAND}' entry to allow tracing.\n")
|
|
96
|
+
else:
|
|
97
|
+
print("\n================================================================")
|
|
98
|
+
print("Add the following to your ~/.codex/config.toml file:")
|
|
99
|
+
print('\nnotify = ["anosys-codex", "run"]\n')
|
|
100
|
+
print("And create ~/.codex/anosys-env.sh with:")
|
|
101
|
+
print(f'export ANOSYS_HOOK_APIKEY="{api_key}"')
|
|
102
|
+
print(f'export ANOSYS_HOOK_ENDPOINT_URL="{INGESTION_URL}"')
|
|
103
|
+
print(f'export REDACTION="{"true" if redaction else "false"}"')
|
|
104
|
+
print("================================================================\n")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_uninstall(args: argparse.Namespace) -> None:
|
|
108
|
+
print("\nAnoSys OpenAI Codex Hook Uninstaller")
|
|
109
|
+
print("=" * 45)
|
|
110
|
+
|
|
111
|
+
config_path = get_config_path()
|
|
112
|
+
env_path = get_env_path()
|
|
113
|
+
|
|
114
|
+
backup_path = backup(config_path)
|
|
115
|
+
removed_config = remove_codex_config(config_path)
|
|
116
|
+
removed_env = remove_codex_env(env_path)
|
|
117
|
+
|
|
118
|
+
if backup_path:
|
|
119
|
+
print(f" Backed up settings -> {backup_path}")
|
|
120
|
+
if removed_config:
|
|
121
|
+
print(f" Removed hook entry from {config_path}")
|
|
122
|
+
else:
|
|
123
|
+
print(f" No AnoSys hook entry found in {config_path}")
|
|
124
|
+
|
|
125
|
+
if removed_env:
|
|
126
|
+
print(f" Removed {env_path}")
|
|
127
|
+
|
|
128
|
+
print("\n✅ AnoSys Codex hook uninstalled successfully.\n")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
132
|
+
config_path = get_config_path()
|
|
133
|
+
env_path = get_env_path()
|
|
134
|
+
|
|
135
|
+
print("\nAnoSys Codex Hook Status")
|
|
136
|
+
print("=" * 45)
|
|
137
|
+
print(f"Codex Config: {config_path}")
|
|
138
|
+
print(f"Env File: {env_path}")
|
|
139
|
+
|
|
140
|
+
if not config_path.is_file():
|
|
141
|
+
print("\n⚠️ Codex config.toml does not exist.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
data = load_toml(config_path)
|
|
145
|
+
installed = has_anosys_hook(data)
|
|
146
|
+
|
|
147
|
+
if installed:
|
|
148
|
+
print("\nStatus: ✅ Installed and active in config.toml")
|
|
149
|
+
notify_list = data.get("notify", [])
|
|
150
|
+
print(f"Notify hooks: {notify_list}")
|
|
151
|
+
else:
|
|
152
|
+
print("\nStatus: ❌ Not registered in config.toml")
|
|
153
|
+
|
|
154
|
+
if env_path.is_file():
|
|
155
|
+
print("Env file exists: ✅ Yes")
|
|
156
|
+
else:
|
|
157
|
+
print("Env file exists: ❌ No")
|
|
158
|
+
print()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_run(args: argparse.Namespace) -> None:
|
|
162
|
+
from anosys_sdk_codex.hook_runner import run
|
|
163
|
+
run()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main(argv: list[str] | None = None) -> None:
|
|
167
|
+
parser = argparse.ArgumentParser(
|
|
168
|
+
prog="anosys-codex",
|
|
169
|
+
description="AnoSys CLI integration for OpenAI Codex",
|
|
170
|
+
)
|
|
171
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
172
|
+
|
|
173
|
+
p_install = subparsers.add_parser("install", help="Install AnoSys hook into ~/.codex/config.toml")
|
|
174
|
+
p_install.add_argument("--api-key", help="AnoSys logs API key")
|
|
175
|
+
p_install.add_argument("-y", "--yes", action="store_true", default=False, help="Automatic yes to prompts (non-interactive)")
|
|
176
|
+
p_install.add_argument("--redaction", action="store_true", default=False, help="Enable content redaction")
|
|
177
|
+
p_install.add_argument("--no-redaction", action="store_true", default=False, help="Disable content redaction")
|
|
178
|
+
p_install.add_argument("--auto-update", dest="auto_update", action="store_true", default=None)
|
|
179
|
+
p_install.add_argument("--no-auto-update", dest="auto_update", action="store_false")
|
|
180
|
+
p_install.set_defaults(func=cmd_install)
|
|
181
|
+
|
|
182
|
+
p_uninstall = subparsers.add_parser("uninstall", help="Remove AnoSys hook from ~/.codex/config.toml")
|
|
183
|
+
p_uninstall.set_defaults(func=cmd_uninstall)
|
|
184
|
+
|
|
185
|
+
p_status = subparsers.add_parser("status", help="Check hook installation status")
|
|
186
|
+
p_status.set_defaults(func=cmd_status)
|
|
187
|
+
|
|
188
|
+
p_run = subparsers.add_parser("run", help="Run the hook (invoked by Codex notify)")
|
|
189
|
+
p_run.set_defaults(func=cmd_run)
|
|
190
|
+
|
|
191
|
+
args = parser.parse_args(argv)
|
|
192
|
+
if not hasattr(args, "func"):
|
|
193
|
+
parser.print_help()
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
args.func(args)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side encryption utilities for AnoSys Codex integration.
|
|
3
|
+
|
|
4
|
+
Supports AES-256-CBC encryption of sensitive telemetry columns
|
|
5
|
+
(prompts, completions, messages, raw context) before sending to AnoSys BE.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, List, Optional, Set
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
ANOSYS_ENCRYPTION_PREFIX = "enc:"
|
|
18
|
+
|
|
19
|
+
DEFAULT_ENCRYPTED_COLUMNS: List[str] = [
|
|
20
|
+
# Primary requested columns
|
|
21
|
+
"llm_input",
|
|
22
|
+
"llm_output",
|
|
23
|
+
"gen_ai_input_messages",
|
|
24
|
+
"gen_ai_output_messages",
|
|
25
|
+
"cvs199",
|
|
26
|
+
# System instructions and legacy system fields
|
|
27
|
+
"gen_ai_system_instructions",
|
|
28
|
+
"llm_system",
|
|
29
|
+
# User prompts and assistant responses across SDKs
|
|
30
|
+
"user_prompt",
|
|
31
|
+
"userPrompt",
|
|
32
|
+
"assistant_text",
|
|
33
|
+
"assistantText",
|
|
34
|
+
"cvs4",
|
|
35
|
+
"cvs5",
|
|
36
|
+
# Multi-turn prompt context and tool execution results
|
|
37
|
+
"last_prompt",
|
|
38
|
+
"cvs24",
|
|
39
|
+
"tool_use_result",
|
|
40
|
+
"cvs26",
|
|
41
|
+
# Additional task / speech prompt fields
|
|
42
|
+
"cvs65",
|
|
43
|
+
"task_description",
|
|
44
|
+
"summary_content",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
49
|
+
from cryptography.hazmat.backends import default_backend
|
|
50
|
+
_HAS_CRYPTOGRAPHY = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
_HAS_CRYPTOGRAPHY = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def derive_256_bit_key(passphrase: str) -> bytes:
|
|
56
|
+
"""Derive a 256-bit (32-byte) key from a passphrase using SHA-256."""
|
|
57
|
+
sha256 = hashlib.sha256()
|
|
58
|
+
sha256.update(passphrase.encode("utf-8"))
|
|
59
|
+
return sha256.digest()[:32]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def encrypt(text: str, key: bytes) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Encrypt text with AES-256-CBC using a zero IV and PKCS#7 padding.
|
|
65
|
+
Returns standard Base64-encoded ciphertext.
|
|
66
|
+
"""
|
|
67
|
+
if not _HAS_CRYPTOGRAPHY:
|
|
68
|
+
raise RuntimeError("The 'cryptography' library is required for encryption. Install with: pip install cryptography")
|
|
69
|
+
|
|
70
|
+
iv = b"\x00" * 16
|
|
71
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
|
72
|
+
encryptor = cipher.encryptor()
|
|
73
|
+
|
|
74
|
+
text_bytes = text.encode("utf-8")
|
|
75
|
+
padding_length = 16 - (len(text_bytes) % 16)
|
|
76
|
+
padded_bytes = text_bytes + (bytes([padding_length]) * padding_length)
|
|
77
|
+
|
|
78
|
+
encrypted = encryptor.update(padded_bytes) + encryptor.finalize()
|
|
79
|
+
return base64.b64encode(encrypted).decode("utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def decrypt(prefixed_or_raw: str, key: bytes) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Decrypt Base64 ciphertext with or without 'enc:' prefix.
|
|
85
|
+
"""
|
|
86
|
+
if not _HAS_CRYPTOGRAPHY:
|
|
87
|
+
raise RuntimeError("The 'cryptography' library is required for decryption. Install with: pip install cryptography")
|
|
88
|
+
|
|
89
|
+
raw_b64 = prefixed_or_raw
|
|
90
|
+
if raw_b64.startswith(ANOSYS_ENCRYPTION_PREFIX):
|
|
91
|
+
raw_b64 = raw_b64[len(ANOSYS_ENCRYPTION_PREFIX):]
|
|
92
|
+
|
|
93
|
+
encrypted_bytes = base64.b64decode(raw_b64)
|
|
94
|
+
iv = b"\x00" * 16
|
|
95
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
|
96
|
+
decryptor = cipher.decryptor()
|
|
97
|
+
decrypted = decryptor.update(encrypted_bytes) + decryptor.finalize()
|
|
98
|
+
if not decrypted:
|
|
99
|
+
return ""
|
|
100
|
+
padding_length = decrypted[-1]
|
|
101
|
+
if padding_length > 16 or padding_length <= 0:
|
|
102
|
+
return decrypted.decode("utf-8")
|
|
103
|
+
return decrypted[:-padding_length].decode("utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_encryption_key() -> Optional[str]:
|
|
107
|
+
"""Retrieve the encryption key from ANOSYS_ENCRYPTION_KEY env var if present and not null."""
|
|
108
|
+
key = os.environ.get("ANOSYS_ENCRYPTION_KEY")
|
|
109
|
+
if not key or not key.strip() or key.strip().lower() in ("null", "none", "undefined"):
|
|
110
|
+
return None
|
|
111
|
+
return key.strip()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_target_columns() -> Set[str]:
|
|
115
|
+
"""
|
|
116
|
+
Retrieve columns to encrypt.
|
|
117
|
+
If ANOSYS_ENCRYPTED_COLUMNS or ANOSYS_ENCRYPTES_COLUMNS is provided,
|
|
118
|
+
it overrides the default list.
|
|
119
|
+
"""
|
|
120
|
+
raw = os.environ.get("ANOSYS_ENCRYPTED_COLUMNS") or os.environ.get("ANOSYS_ENCRYPTES_COLUMNS")
|
|
121
|
+
if raw and raw.strip():
|
|
122
|
+
cols = {col.strip() for col in raw.split(",") if col.strip()}
|
|
123
|
+
if cols:
|
|
124
|
+
return cols
|
|
125
|
+
return set(DEFAULT_ENCRYPTED_COLUMNS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def encrypt_payload(data: Any) -> Any:
|
|
129
|
+
"""
|
|
130
|
+
Encrypt targeted fields in a payload dict (or list of dicts) if ANOSYS_ENCRYPTION_KEY is configured.
|
|
131
|
+
Warns on non-string values and skips them.
|
|
132
|
+
"""
|
|
133
|
+
if data is None:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if isinstance(data, list):
|
|
137
|
+
return [encrypt_payload(item) for item in data]
|
|
138
|
+
|
|
139
|
+
if not isinstance(data, dict):
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
key_str = get_encryption_key()
|
|
143
|
+
if not key_str:
|
|
144
|
+
return data
|
|
145
|
+
|
|
146
|
+
if not _HAS_CRYPTOGRAPHY:
|
|
147
|
+
msg = "[ANOSYS] Warning: ANOSYS_ENCRYPTION_KEY is set but 'cryptography' library is not installed. Skipping encryption."
|
|
148
|
+
print(msg, file=sys.stderr)
|
|
149
|
+
logger.warning(msg)
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
derived_key = derive_256_bit_key(key_str)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
msg = f"[ANOSYS] Warning: Failed to derive encryption key: {e}"
|
|
156
|
+
print(msg, file=sys.stderr)
|
|
157
|
+
logger.warning(msg)
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
target_columns = get_target_columns()
|
|
161
|
+
|
|
162
|
+
for col in target_columns:
|
|
163
|
+
if col not in data:
|
|
164
|
+
continue
|
|
165
|
+
val = data[col]
|
|
166
|
+
if val is None:
|
|
167
|
+
continue
|
|
168
|
+
if not isinstance(val, str):
|
|
169
|
+
warn_msg = f"[ANOSYS] Warning: Column '{col}' contains non-string data (type: {type(val).__name__}). Skipping encryption."
|
|
170
|
+
print(warn_msg, file=sys.stderr)
|
|
171
|
+
logger.warning(warn_msg)
|
|
172
|
+
continue
|
|
173
|
+
if val.startswith(ANOSYS_ENCRYPTION_PREFIX):
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
data[col] = f"{ANOSYS_ENCRYPTION_PREFIX}{encrypt(val, derived_key)}"
|
|
177
|
+
except Exception as e:
|
|
178
|
+
warn_msg = f"[ANOSYS] Warning: Failed to encrypt column '{col}': {e}"
|
|
179
|
+
print(warn_msg, file=sys.stderr)
|
|
180
|
+
logger.warning(warn_msg)
|
|
181
|
+
|
|
182
|
+
return data
|