openrunner-sdk 2.7.0__tar.gz → 2.8.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.
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/PKG-INFO +2 -1
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/cli.py +7 -2
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/install_commands.py +65 -43
- openrunner_sdk-2.8.0/openrunner/redact.py +339 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/run.py +1 -9
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/session.py +157 -35
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/pyproject.toml +2 -2
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/.gitignore +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/=6.0 +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/=8.1 +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/README.md +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/conftest.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_cli.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.7.0 → openrunner_sdk-2.8.0}/tests/test_wandb_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrunner-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
|
|
5
5
|
Project-URL: Homepage, https://github.com/jqueguiner/openrunner
|
|
6
6
|
Project-URL: Repository, https://github.com/jqueguiner/openrunner
|
|
@@ -33,6 +33,7 @@ Requires-Dist: catboost>=1.2; extra == 'catboost'
|
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: numpy>=1.24; extra == 'dev'
|
|
35
35
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
38
|
Provides-Extra: diffusers
|
|
38
39
|
Requires-Dist: diffusers>=0.25; extra == 'diffusers'
|
|
@@ -120,7 +120,7 @@ launch.from_run = _launch_from_run # type: ignore[attr-defined]
|
|
|
120
120
|
# openrunner.trace.patch_openai() syntax
|
|
121
121
|
trace.patch_openai = _patch_openai # type: ignore[attr-defined]
|
|
122
122
|
|
|
123
|
-
__version__ = "2.
|
|
123
|
+
__version__ = "2.8.0"
|
|
124
124
|
|
|
125
125
|
logger = logging.getLogger("openrunner")
|
|
126
126
|
|
|
@@ -2391,13 +2391,18 @@ def session_setup() -> None:
|
|
|
2391
2391
|
@click.option("--hours", "-h", default=24.0, help="Look back N hours (default: 24)")
|
|
2392
2392
|
@click.option("--project", "-p", default=None, help="Target project (default: from config)")
|
|
2393
2393
|
@click.option("--dry-run", is_flag=True, help="Show what would be synced without uploading")
|
|
2394
|
-
|
|
2394
|
+
@click.option("--redact/--no-redact", default=None, help="Force redaction on/off (default: use config)")
|
|
2395
|
+
@click.option("--redact-mode", type=click.Choice(["regex", "ner"]), default=None, help="Redaction mode")
|
|
2396
|
+
@click.option("--public", "visibility", flag_value="public", help="Make session public")
|
|
2397
|
+
@click.option("--private", "visibility", flag_value="private", default=True, help="Keep session private (default)")
|
|
2398
|
+
def session_sync(directory: str | None, hours: float, project: str | None, dry_run: bool, redact: bool | None, redact_mode: str | None, visibility: str) -> None:
|
|
2395
2399
|
"""Sync AI sessions to OpenRunner.
|
|
2396
2400
|
|
|
2397
2401
|
If DIRECTORY is given, scan that path for .jsonl/.json session files.
|
|
2398
2402
|
Otherwise, scan default locations (~/.claude, ~/.codex, ~/.qwen-code).
|
|
2399
2403
|
|
|
2400
2404
|
On first run, prompts for API key and project selection.
|
|
2405
|
+
Redaction strips API keys, tokens, emails, passwords before upload.
|
|
2401
2406
|
"""
|
|
2402
2407
|
from pathlib import Path
|
|
2403
2408
|
from openrunner.session import discover_all_sessions, discover_in_directory, sync_all, get_session_config, interactive_setup
|
|
@@ -2427,7 +2432,7 @@ def session_sync(directory: str | None, hours: float, project: str | None, dry_r
|
|
|
2427
2432
|
if dry_run:
|
|
2428
2433
|
return
|
|
2429
2434
|
|
|
2430
|
-
synced = sync_all(since_hours=hours, project=project, directory=Path(directory) if directory else None)
|
|
2435
|
+
synced = sync_all(since_hours=hours, project=project, directory=Path(directory) if directory else None, redact=redact, redact_mode=redact_mode, visibility=visibility)
|
|
2431
2436
|
if synced:
|
|
2432
2437
|
click.echo(f"Synced {len(synced)} session(s) to OpenRunner.")
|
|
2433
2438
|
for run_id in synced:
|
|
@@ -26,68 +26,90 @@ from pathlib import Path
|
|
|
26
26
|
SYNC_SESSION_CMD = """---
|
|
27
27
|
name: {prefix}sync-session
|
|
28
28
|
description: Sync current coding session to OpenRunner as a research log
|
|
29
|
+
argument-hint: "[org/project]"
|
|
30
|
+
allowed-tools:
|
|
31
|
+
- Bash
|
|
32
|
+
- Read
|
|
33
|
+
- AskUserQuestion
|
|
29
34
|
---
|
|
30
35
|
|
|
31
|
-
Sync the current session to OpenRunner.
|
|
36
|
+
Sync the current Claude Code session to OpenRunner.
|
|
37
|
+
|
|
38
|
+
## Process
|
|
39
|
+
|
|
40
|
+
### Step 1: Check configuration
|
|
41
|
+
|
|
42
|
+
Run this to check if session config exists:
|
|
32
43
|
|
|
33
44
|
```bash
|
|
34
45
|
python3 -c "
|
|
35
|
-
import
|
|
46
|
+
import json
|
|
36
47
|
from pathlib import Path
|
|
48
|
+
config_file = Path.home() / '.openrunner' / 'session_config.json'
|
|
49
|
+
if config_file.exists():
|
|
50
|
+
config = json.loads(config_file.read_text())
|
|
51
|
+
if config.get('api_key') and config.get('project'):
|
|
52
|
+
print('CONFIGURED')
|
|
53
|
+
print(f'project={{config[\"project\"]}}')
|
|
54
|
+
else:
|
|
55
|
+
print('INCOMPLETE')
|
|
56
|
+
else:
|
|
57
|
+
print('NOT_CONFIGURED')
|
|
58
|
+
"
|
|
59
|
+
```
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
|
|
40
|
-
os.path.expanduser('~/.local/lib/python3.11/site-packages'),
|
|
41
|
-
os.path.expanduser('~/.local/lib/python3.10/site-packages')]:
|
|
42
|
-
if os.path.isdir(p):
|
|
43
|
-
sys.path.insert(0, p)
|
|
61
|
+
### Step 2a: If NOT_CONFIGURED or INCOMPLETE
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Find session dir for current tool
|
|
48
|
-
session_dirs = [
|
|
49
|
-
Path.home() / '.claude' / 'projects',
|
|
50
|
-
Path.home() / '.codex' / 'sessions',
|
|
51
|
-
Path.home() / '.qwen-code' / 'sessions',
|
|
52
|
-
Path.home() / '.opencode' / 'sessions',
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
# Find most recent session file across all sources
|
|
56
|
-
all_sessions = []
|
|
57
|
-
for d in session_dirs:
|
|
58
|
-
if d.exists():
|
|
59
|
-
for f in d.rglob('*.jsonl'):
|
|
60
|
-
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
61
|
-
all_sessions.append(f)
|
|
62
|
-
for f in d.rglob('*.json'):
|
|
63
|
-
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
64
|
-
all_sessions.append(f)
|
|
65
|
-
|
|
66
|
-
if not all_sessions:
|
|
67
|
-
print('No sessions found.')
|
|
68
|
-
sys.exit(1)
|
|
63
|
+
Ask the user for their OpenRunner API key and base URL. Then list projects and ask them to pick one. Save to ~/.openrunner/session_config.json.
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
|
|
65
|
+
### Step 2b: If CONFIGURED (or after setup)
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
Sync the current session. Use $ARGUMENTS as project override if provided:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -c "
|
|
71
|
+
import sys, os
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
for p in [os.path.expanduser(f'~/.local/lib/python3.{{v}}/site-packages') for v in (12,11,10)]:
|
|
74
|
+
if os.path.isdir(p): sys.path.insert(0, p)
|
|
75
|
+
from openrunner.session import parse_claude_session, sync_session_to_openrunner, get_session_config
|
|
76
|
+
|
|
77
|
+
cwd = Path.cwd()
|
|
78
|
+
cwd_key = '-' + str(cwd).replace('/', '-').lstrip('-')
|
|
79
|
+
project_dir = Path.home() / '.claude' / 'projects' / cwd_key
|
|
80
|
+
if not project_dir.exists():
|
|
81
|
+
for d in (Path.home() / '.claude' / 'projects').iterdir():
|
|
82
|
+
if d.is_dir() and cwd_key in d.name:
|
|
83
|
+
project_dir = d
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
sessions = sorted(
|
|
87
|
+
[f for f in project_dir.rglob('*.jsonl') if f.stat().st_size > 100 and '.meta.' not in f.name],
|
|
88
|
+
key=lambda f: f.stat().st_mtime, reverse=True,
|
|
89
|
+
) if project_dir.exists() else []
|
|
90
|
+
|
|
91
|
+
if not sessions:
|
|
92
|
+
print('No session files found.')
|
|
93
|
+
sys.exit(1)
|
|
78
94
|
|
|
95
|
+
latest = sessions[0]
|
|
96
|
+
print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
|
|
97
|
+
parsed = parse_claude_session(latest)
|
|
79
98
|
print(f' Messages: {{parsed[\"message_count\"]}} ({{parsed[\"user_message_count\"]}} user)')
|
|
80
99
|
print(f' Tokens: {{parsed.get(\"total_tokens\", 0):,}}')
|
|
81
100
|
|
|
82
|
-
|
|
83
|
-
run_id = sync_session_to_openrunner(parsed, project=
|
|
101
|
+
project_override = '$ARGUMENTS'.strip() or None
|
|
102
|
+
run_id = sync_session_to_openrunner(parsed, project=project_override)
|
|
84
103
|
if run_id:
|
|
85
|
-
|
|
104
|
+
config = get_session_config()
|
|
105
|
+
base = config.get('base_url', 'https://openrun.gladia.io')
|
|
86
106
|
print(f'Synced -> {{base}}/runs/{{run_id}}')
|
|
87
107
|
else:
|
|
88
|
-
print('
|
|
108
|
+
print('Sync failed. Run: openrunner session setup')
|
|
89
109
|
"
|
|
90
110
|
```
|
|
111
|
+
|
|
112
|
+
### Step 3: Report the run URL and stats to the user.
|
|
91
113
|
"""
|
|
92
114
|
|
|
93
115
|
LOG_NOTE_CMD = """---
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Session redaction — detect and mask PII/secrets before sync.
|
|
2
|
+
|
|
3
|
+
Inspired by Dataiku's kiji-proxy (DeBERTa NER + synthetic replacement).
|
|
4
|
+
|
|
5
|
+
Two modes:
|
|
6
|
+
1. Regex-based (fast, no deps): API keys, tokens, passwords, IPs, emails, paths
|
|
7
|
+
2. NER-based (accurate, needs transformers): full PII detection via DeBERTa
|
|
8
|
+
|
|
9
|
+
Redaction can be configured at:
|
|
10
|
+
- Client side: per-sync via `openrunner session sync --redact`
|
|
11
|
+
- Organization level: org setting forces redaction for all members
|
|
12
|
+
- User level: user setting in session_config.json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Regex patterns for secrets and common PII
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
SECRET_PATTERNS: list[tuple[str, re.Pattern]] = [
|
|
27
|
+
# API keys / tokens (generic patterns)
|
|
28
|
+
("API_KEY", re.compile(r"\b(sk-[a-zA-Z0-9\-_]{20,})\b")), # OpenAI
|
|
29
|
+
("API_KEY", re.compile(r"\b(or_[a-zA-Z0-9_\-]{20,})\b")), # OpenRunner
|
|
30
|
+
("API_KEY", re.compile(r"\b(ghp_[a-zA-Z0-9]{36,})\b")), # GitHub PAT
|
|
31
|
+
("API_KEY", re.compile(r"\b(gho_[a-zA-Z0-9]{36,})\b")), # GitHub OAuth
|
|
32
|
+
("API_KEY", re.compile(r"\b(github_pat_[a-zA-Z0-9_]{40,})\b")), # GitHub fine-grained
|
|
33
|
+
("API_KEY", re.compile(r"\b(pypi-[a-zA-Z0-9_\-]{50,})\b")), # PyPI
|
|
34
|
+
("API_KEY", re.compile(r"\b(npm_[a-zA-Z0-9]{30,})\b")), # npm
|
|
35
|
+
("API_KEY", re.compile(r"\b(xox[bsapr]-[a-zA-Z0-9\-]{10,})\b")), # Slack
|
|
36
|
+
("API_KEY", re.compile(r"\b(AKIA[0-9A-Z]{16})\b")), # AWS access key
|
|
37
|
+
("SECRET", re.compile(r"\b([a-zA-Z0-9/+=]{40})\b(?=.*(?:secret|SECRET))")), # AWS secret
|
|
38
|
+
("API_KEY", re.compile(r"\b(AIza[0-9A-Za-z_\-]{35})\b")), # Google API
|
|
39
|
+
("TOKEN", re.compile(r"\b(eyJ[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{20,})\b")), # JWT
|
|
40
|
+
# Passwords in config/env
|
|
41
|
+
("PASSWORD", re.compile(r"(?i)(?:password|passwd|pwd)\s*[=:]\s*['\"]?([^\s'\"]{6,})['\"]?")),
|
|
42
|
+
# Connection strings
|
|
43
|
+
("CONNECTION_STRING", re.compile(r"(?i)((?:postgres|mysql|mongodb|redis)://[^\s'\"]+)")),
|
|
44
|
+
# Private keys
|
|
45
|
+
("PRIVATE_KEY", re.compile(r"(-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[^-]+-----END (?:RSA |EC |DSA )?PRIVATE KEY-----)", re.DOTALL)),
|
|
46
|
+
# Email addresses
|
|
47
|
+
("EMAIL", re.compile(r"\b([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})\b")),
|
|
48
|
+
# IP addresses (non-localhost, non-docker)
|
|
49
|
+
("IP_ADDRESS", re.compile(r"\b((?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d))\b")),
|
|
50
|
+
# Home directory paths (contain username)
|
|
51
|
+
("PATH", re.compile(r"(/(?:home|Users)/[a-zA-Z0-9._\-]+)")),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# IPs to NOT redact (internal/docker/localhost)
|
|
55
|
+
SAFE_IPS = {"127.0.0.1", "0.0.0.0", "localhost", "172.17.0.1", "172.18.0.1"}
|
|
56
|
+
SAFE_IP_PREFIXES = ("10.", "172.16.", "172.17.", "172.18.", "192.168.")
|
|
57
|
+
|
|
58
|
+
# Emails to NOT redact
|
|
59
|
+
SAFE_EMAILS = {"noreply@anthropic.com", "noreply@github.com"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_safe_ip(ip: str) -> bool:
|
|
63
|
+
return ip in SAFE_IPS or any(ip.startswith(p) for p in SAFE_IP_PREFIXES)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _generate_replacement(label: str, original: str) -> str:
|
|
67
|
+
"""Generate a deterministic replacement (same input -> same output)."""
|
|
68
|
+
# Use hash to generate consistent replacement
|
|
69
|
+
h = hashlib.sha256(original.encode()).hexdigest()[:8]
|
|
70
|
+
|
|
71
|
+
if label == "API_KEY":
|
|
72
|
+
return f"REDACTED_KEY_{h}"
|
|
73
|
+
elif label == "SECRET":
|
|
74
|
+
return f"REDACTED_SECRET_{h}"
|
|
75
|
+
elif label == "TOKEN":
|
|
76
|
+
return f"REDACTED_TOKEN_{h}"
|
|
77
|
+
elif label == "PASSWORD":
|
|
78
|
+
return f"REDACTED_PASS_{h}"
|
|
79
|
+
elif label == "CONNECTION_STRING":
|
|
80
|
+
# Keep protocol, redact rest
|
|
81
|
+
proto = original.split("://")[0] if "://" in original else "db"
|
|
82
|
+
return f"{proto}://REDACTED_{h}"
|
|
83
|
+
elif label == "PRIVATE_KEY":
|
|
84
|
+
return "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----"
|
|
85
|
+
elif label == "EMAIL":
|
|
86
|
+
domain = original.split("@")[1] if "@" in original else "example.com"
|
|
87
|
+
return f"user_{h[:4]}@{domain}"
|
|
88
|
+
elif label == "IP_ADDRESS":
|
|
89
|
+
return f"x.x.x.{h[:2]}"
|
|
90
|
+
elif label == "PATH":
|
|
91
|
+
return f"/home/user_{h[:4]}"
|
|
92
|
+
else:
|
|
93
|
+
return f"[REDACTED:{label}]"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Core redaction engine
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RedactionResult:
|
|
102
|
+
"""Result of redacting text."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, text: str, entities: list[dict], mapping: dict[str, str]):
|
|
105
|
+
self.text = text
|
|
106
|
+
self.entities = entities # [{label, start, end, original, replacement}]
|
|
107
|
+
self.mapping = mapping # original -> replacement (for restoration)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def redacted_count(self) -> int:
|
|
111
|
+
return len(self.entities)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def redact_text(text: str, mode: str = "regex") -> RedactionResult:
|
|
115
|
+
"""Redact sensitive content from text.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
text: Input text to redact.
|
|
119
|
+
mode: "regex" (fast, pattern-based) or "ner" (ML-based, needs transformers).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
RedactionResult with redacted text and metadata.
|
|
123
|
+
"""
|
|
124
|
+
if mode == "ner":
|
|
125
|
+
return _redact_ner(text)
|
|
126
|
+
return _redact_regex(text)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _redact_regex(text: str) -> RedactionResult:
|
|
130
|
+
"""Fast regex-based redaction for secrets and common PII."""
|
|
131
|
+
entities = []
|
|
132
|
+
|
|
133
|
+
for label, pattern in SECRET_PATTERNS:
|
|
134
|
+
for match in pattern.finditer(text):
|
|
135
|
+
original = match.group(1) if match.lastindex else match.group(0)
|
|
136
|
+
start = match.start(1) if match.lastindex else match.start(0)
|
|
137
|
+
end = match.end(1) if match.lastindex else match.end(0)
|
|
138
|
+
|
|
139
|
+
# Skip safe values
|
|
140
|
+
if label == "IP_ADDRESS" and _is_safe_ip(original):
|
|
141
|
+
continue
|
|
142
|
+
if label == "EMAIL" and original.lower() in SAFE_EMAILS:
|
|
143
|
+
continue
|
|
144
|
+
# Skip very short matches (likely false positives)
|
|
145
|
+
if len(original) < 6:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
entities.append({
|
|
149
|
+
"label": label,
|
|
150
|
+
"start": start,
|
|
151
|
+
"end": end,
|
|
152
|
+
"original": original,
|
|
153
|
+
"replacement": _generate_replacement(label, original),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# Deduplicate overlapping entities (keep longest)
|
|
157
|
+
entities.sort(key=lambda e: (e["start"], -(e["end"] - e["start"])))
|
|
158
|
+
deduped = []
|
|
159
|
+
last_end = -1
|
|
160
|
+
for e in entities:
|
|
161
|
+
if e["start"] >= last_end:
|
|
162
|
+
deduped.append(e)
|
|
163
|
+
last_end = e["end"]
|
|
164
|
+
|
|
165
|
+
# Apply replacements (end-to-start to preserve offsets)
|
|
166
|
+
result_text = text
|
|
167
|
+
mapping = {}
|
|
168
|
+
for e in reversed(deduped):
|
|
169
|
+
result_text = result_text[:e["start"]] + e["replacement"] + result_text[e["end"]:]
|
|
170
|
+
mapping[e["original"]] = e["replacement"]
|
|
171
|
+
|
|
172
|
+
return RedactionResult(result_text, deduped, mapping)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _redact_ner(text: str) -> RedactionResult:
|
|
176
|
+
"""NER-based redaction using DeBERTa model (DataikuNLP/kiji-pii-model).
|
|
177
|
+
|
|
178
|
+
Falls back to regex if transformers not installed.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
from transformers import pipeline
|
|
182
|
+
except ImportError:
|
|
183
|
+
return _redact_regex(text)
|
|
184
|
+
|
|
185
|
+
# Load model (cached after first call)
|
|
186
|
+
global _ner_pipeline
|
|
187
|
+
if "_ner_pipeline" not in globals() or _ner_pipeline is None:
|
|
188
|
+
try:
|
|
189
|
+
_ner_pipeline = pipeline(
|
|
190
|
+
"token-classification",
|
|
191
|
+
model="DataikuNLP/kiji-pii-model-onnx",
|
|
192
|
+
aggregation_strategy="simple",
|
|
193
|
+
)
|
|
194
|
+
except Exception:
|
|
195
|
+
# Fall back to regex if model load fails
|
|
196
|
+
return _redact_regex(text)
|
|
197
|
+
|
|
198
|
+
# Run NER
|
|
199
|
+
try:
|
|
200
|
+
ner_results = _ner_pipeline(text[:10000]) # Cap at 10k chars
|
|
201
|
+
except Exception:
|
|
202
|
+
return _redact_regex(text)
|
|
203
|
+
|
|
204
|
+
entities = []
|
|
205
|
+
for ent in ner_results:
|
|
206
|
+
if ent.get("score", 0) < 0.25:
|
|
207
|
+
continue
|
|
208
|
+
label = ent.get("entity_group", ent.get("entity", "UNKNOWN"))
|
|
209
|
+
original = ent.get("word", "")
|
|
210
|
+
entities.append({
|
|
211
|
+
"label": label,
|
|
212
|
+
"start": ent["start"],
|
|
213
|
+
"end": ent["end"],
|
|
214
|
+
"original": original,
|
|
215
|
+
"replacement": _generate_replacement(label, original),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
# Also run regex for secrets (NER won't catch API keys)
|
|
219
|
+
regex_result = _redact_regex(text)
|
|
220
|
+
# Merge: add regex entities that don't overlap with NER
|
|
221
|
+
for re_ent in regex_result.entities:
|
|
222
|
+
overlaps = any(
|
|
223
|
+
re_ent["start"] < e["end"] and re_ent["end"] > e["start"]
|
|
224
|
+
for e in entities
|
|
225
|
+
)
|
|
226
|
+
if not overlaps:
|
|
227
|
+
entities.append(re_ent)
|
|
228
|
+
|
|
229
|
+
entities.sort(key=lambda e: e["start"])
|
|
230
|
+
|
|
231
|
+
# Apply replacements
|
|
232
|
+
result_text = text
|
|
233
|
+
mapping = {}
|
|
234
|
+
for e in reversed(entities):
|
|
235
|
+
result_text = result_text[:e["start"]] + e["replacement"] + result_text[e["end"]:]
|
|
236
|
+
mapping[e["original"]] = e["replacement"]
|
|
237
|
+
|
|
238
|
+
return RedactionResult(result_text, entities, mapping)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Session-level redaction
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def redact_session(parsed: dict[str, Any], mode: str = "regex") -> dict[str, Any]:
|
|
247
|
+
"""Redact a parsed session dict before sync.
|
|
248
|
+
|
|
249
|
+
Redacts:
|
|
250
|
+
- All message content (user + assistant)
|
|
251
|
+
- File paths (replace usernames)
|
|
252
|
+
- First message / summary
|
|
253
|
+
|
|
254
|
+
Returns a new dict (doesn't mutate input).
|
|
255
|
+
"""
|
|
256
|
+
import copy
|
|
257
|
+
result = copy.deepcopy(parsed)
|
|
258
|
+
|
|
259
|
+
total_redacted = 0
|
|
260
|
+
|
|
261
|
+
# Redact messages
|
|
262
|
+
for msg in result.get("messages", []):
|
|
263
|
+
if msg.get("content"):
|
|
264
|
+
r = redact_text(msg["content"], mode=mode)
|
|
265
|
+
msg["content"] = r.text
|
|
266
|
+
total_redacted += r.redacted_count
|
|
267
|
+
|
|
268
|
+
# Redact first_message
|
|
269
|
+
if result.get("first_message"):
|
|
270
|
+
r = redact_text(result["first_message"], mode=mode)
|
|
271
|
+
result["first_message"] = r.text
|
|
272
|
+
total_redacted += r.redacted_count
|
|
273
|
+
|
|
274
|
+
# Redact summary
|
|
275
|
+
if result.get("summary"):
|
|
276
|
+
r = redact_text(result["summary"], mode=mode)
|
|
277
|
+
result["summary"] = r.text
|
|
278
|
+
total_redacted += r.redacted_count
|
|
279
|
+
|
|
280
|
+
# Redact file paths (just home dir usernames)
|
|
281
|
+
if result.get("files_touched"):
|
|
282
|
+
result["files_touched"] = [
|
|
283
|
+
re.sub(r"/(?:home|Users)/[^/]+", "/home/user", f)
|
|
284
|
+
for f in result["files_touched"]
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
result["_redaction"] = {
|
|
288
|
+
"mode": mode,
|
|
289
|
+
"entities_redacted": total_redacted,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# Redaction policy config
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class RedactionPolicy:
|
|
301
|
+
"""Redaction policy: determines if/how to redact based on config."""
|
|
302
|
+
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
enabled: bool = False,
|
|
306
|
+
mode: str = "regex", # "regex" or "ner"
|
|
307
|
+
force: bool = False, # org-level forced redaction
|
|
308
|
+
):
|
|
309
|
+
self.enabled = enabled
|
|
310
|
+
self.mode = mode
|
|
311
|
+
self.force = force
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def from_config(cls, config: dict) -> "RedactionPolicy":
|
|
315
|
+
"""Load policy from session config or org settings."""
|
|
316
|
+
redaction = config.get("redaction", {})
|
|
317
|
+
return cls(
|
|
318
|
+
enabled=redaction.get("enabled", False),
|
|
319
|
+
mode=redaction.get("mode", "regex"),
|
|
320
|
+
force=redaction.get("force", False),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
def from_org_settings(cls, org_settings: dict) -> "RedactionPolicy":
|
|
325
|
+
"""Load policy from organization-level settings."""
|
|
326
|
+
if org_settings.get("force_session_redaction"):
|
|
327
|
+
return cls(enabled=True, mode=org_settings.get("redaction_mode", "regex"), force=True)
|
|
328
|
+
return cls(enabled=False)
|
|
329
|
+
|
|
330
|
+
def should_redact(self, user_choice: bool | None = None) -> bool:
|
|
331
|
+
"""Determine if redaction should be applied.
|
|
332
|
+
|
|
333
|
+
Priority: org force > user explicit choice > config default.
|
|
334
|
+
"""
|
|
335
|
+
if self.force:
|
|
336
|
+
return True
|
|
337
|
+
if user_choice is not None:
|
|
338
|
+
return user_choice
|
|
339
|
+
return self.enabled
|
|
@@ -1188,15 +1188,7 @@ class Run:
|
|
|
1188
1188
|
Returns:
|
|
1189
1189
|
Path to the local artifact directory, or None on failure.
|
|
1190
1190
|
"""
|
|
1191
|
-
|
|
1192
|
-
return None
|
|
1193
|
-
return self._client.download_artifact(
|
|
1194
|
-
run_id=self._run_id,
|
|
1195
|
-
artifact_name=name,
|
|
1196
|
-
dest_dir=dest_dir,
|
|
1197
|
-
version=version,
|
|
1198
|
-
alias=alias,
|
|
1199
|
-
)
|
|
1191
|
+
return self.use_artifact(name, version=version, alias=alias)
|
|
1200
1192
|
|
|
1201
1193
|
def link_model(
|
|
1202
1194
|
self,
|