stravinsky 0.2.67__py3-none-any.whl → 0.4.18__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.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

mcp_bridge/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.67"
1
+ __version__ = "0.4.18"
@@ -8,10 +8,12 @@ Stores OAuth tokens securely using the OS keyring:
8
8
  """
9
9
 
10
10
  import json
11
+ import os
11
12
  import time
13
+ from pathlib import Path
12
14
  from typing import TypedDict
13
15
 
14
- import keyring
16
+ from cryptography.fernet import Fernet
15
17
 
16
18
 
17
19
  class TokenData(TypedDict, total=False):
@@ -26,13 +28,15 @@ class TokenData(TypedDict, total=False):
26
28
 
27
29
  class TokenStore:
28
30
  """
29
- Secure storage for OAuth tokens using system keyring.
31
+ Secure storage for OAuth tokens using system keyring with encrypted file fallback.
30
32
 
31
33
  Each provider (gemini, openai) stores its tokens separately.
32
34
  Tokens are serialized as JSON for storage.
35
+ Falls back to encrypted file storage if keyring fails.
33
36
  """
34
37
 
35
38
  SERVICE_NAME = "stravinsky"
39
+ FALLBACK_DIR = Path.home() / ".stravinsky" / "tokens"
36
40
 
37
41
  def __init__(self, service_name: str | None = None):
38
42
  """Initialize the token store.
@@ -41,6 +45,63 @@ class TokenStore:
41
45
  service_name: Override the default service name for testing.
42
46
  """
43
47
  self.service_name = service_name or self.SERVICE_NAME
48
+ self._init_fallback_storage()
49
+
50
+ def _init_fallback_storage(self) -> None:
51
+ """Initialize encrypted file storage fallback directory."""
52
+ self.FALLBACK_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ def _get_fallback_path(self, provider: str) -> Path:
55
+ """Get the path for encrypted fallback storage."""
56
+ return self.FALLBACK_DIR / f"{provider}.enc"
57
+
58
+ def _get_or_create_key(self) -> bytes:
59
+ """Get or create encryption key for fallback storage."""
60
+ key_file = self.FALLBACK_DIR / ".key"
61
+ if key_file.exists():
62
+ return key_file.read_bytes()
63
+ # Create new key and save it
64
+ key = Fernet.generate_key()
65
+ key_file.write_bytes(key)
66
+ key_file.chmod(0o600) # Read/write for owner only
67
+ return key
68
+
69
+ def _save_encrypted(self, provider: str, data: str) -> None:
70
+ """Save data to encrypted file."""
71
+ try:
72
+ key = self._get_or_create_key()
73
+ cipher = Fernet(key)
74
+ encrypted = cipher.encrypt(data.encode())
75
+ path = self._get_fallback_path(provider)
76
+ path.write_bytes(encrypted)
77
+ path.chmod(0o600)
78
+ except Exception as e:
79
+ raise RuntimeError(f"Failed to save encrypted token: {e}")
80
+
81
+ def _load_encrypted(self, provider: str) -> str | None:
82
+ """Load data from encrypted file."""
83
+ try:
84
+ path = self._get_fallback_path(provider)
85
+ if not path.exists():
86
+ return None
87
+ key = self._get_or_create_key()
88
+ cipher = Fernet(key)
89
+ encrypted = path.read_bytes()
90
+ decrypted = cipher.decrypt(encrypted)
91
+ return decrypted.decode()
92
+ except Exception:
93
+ return None
94
+
95
+ def _delete_encrypted(self, provider: str) -> bool:
96
+ """Delete encrypted token file."""
97
+ try:
98
+ path = self._get_fallback_path(provider)
99
+ if path.exists():
100
+ path.unlink()
101
+ return True
102
+ return False
103
+ except Exception:
104
+ return False
44
105
 
45
106
  def _key(self, provider: str) -> str:
46
107
  """Generate the keyring key for a provider."""
@@ -56,13 +117,24 @@ class TokenStore:
56
117
  Returns:
57
118
  TokenData if found and valid, None otherwise.
58
119
  """
120
+ # Try keyring first (import inside try to catch backend initialization errors)
59
121
  try:
122
+ import keyring
60
123
  data = keyring.get_password(self.service_name, self._key(provider))
61
- if not data:
62
- return None
63
- return json.loads(data)
64
- except (json.JSONDecodeError, keyring.errors.KeyringError):
65
- return None
124
+ if data:
125
+ return json.loads(data)
126
+ except Exception:
127
+ pass # Fall back to encrypted file (catches KeyringError, import errors, etc.)
128
+
129
+ # Fall back to encrypted file storage
130
+ try:
131
+ data = self._load_encrypted(provider)
132
+ if data:
133
+ return json.loads(data)
134
+ except json.JSONDecodeError:
135
+ pass
136
+
137
+ return None
66
138
 
67
139
  def set_token(
68
140
  self,
@@ -77,6 +149,7 @@ class TokenStore:
77
149
  Store a token for a provider.
78
150
 
79
151
  Can be called with a TokenData dict or individual parameters.
152
+ Falls back to encrypted file storage if keyring fails.
80
153
 
81
154
  Args:
82
155
  provider: The provider name (e.g., 'gemini', 'openai')
@@ -96,7 +169,27 @@ class TokenStore:
96
169
  if expires_at:
97
170
  token_data["expires_at"] = expires_at
98
171
  data = json.dumps(token_data)
99
- keyring.set_password(self.service_name, self._key(provider), data)
172
+
173
+ # Try keyring first, but always also write to encrypted storage
174
+ # Import inside try to catch backend initialization errors (e.g., "No recommended backend")
175
+ keyring_failed = False
176
+ try:
177
+ import keyring
178
+ keyring.set_password(self.service_name, self._key(provider), data)
179
+ except Exception:
180
+ # Keyring failed - fall back to encrypted file
181
+ keyring_failed = True
182
+
183
+ # Always write to encrypted file storage as fallback
184
+ try:
185
+ self._save_encrypted(provider, data)
186
+ except Exception as e:
187
+ # Only fail if both backends failed
188
+ if keyring_failed:
189
+ raise RuntimeError(
190
+ f"Failed to save token to both keyring and encrypted storage: {e}"
191
+ )
192
+
100
193
 
101
194
  def delete_token(self, provider: str) -> bool:
102
195
  """
@@ -108,11 +201,20 @@ class TokenStore:
108
201
  Returns:
109
202
  True if deleted, False if not found.
110
203
  """
204
+ # Try keyring first (import inside try to catch backend initialization errors)
205
+ deleted_from_keyring = False
111
206
  try:
207
+ import keyring
112
208
  keyring.delete_password(self.service_name, self._key(provider))
113
- return True
114
- except keyring.errors.PasswordDeleteError:
115
- return False
209
+ deleted_from_keyring = True
210
+ except Exception:
211
+ # Keyring failed (no backend, delete error, etc.) - continue to encrypted file
212
+ pass
213
+
214
+ # Also delete from encrypted file storage
215
+ deleted_from_file = self._delete_encrypted(provider)
216
+
217
+ return deleted_from_keyring or deleted_from_file
116
218
 
117
219
  def has_valid_token(self, provider: str) -> bool:
118
220
  """
@@ -0,0 +1,305 @@
1
+ # Stravinsky Manifest Schema Documentation
2
+
3
+ ## Overview
4
+
5
+ The manifest files (`hooks_manifest.json` and `skills_manifest.json`) track version information, integrity, and metadata for all Stravinsky-provided hooks and skills. These manifests enable:
6
+
7
+ - **Integrity Verification**: Detect unauthorized or accidental modifications
8
+ - **Update Management**: Determine which files need updating during package upgrades
9
+ - **User Customization**: Distinguish between official Stravinsky files and user modifications
10
+ - **Dependency Tracking**: Understand hook dependencies and required relationships
11
+
12
+ ## File Locations
13
+
14
+ ```
15
+ mcp_bridge/config/
16
+ ├── hooks_manifest.json # Official hook metadata
17
+ ├── skills_manifest.json # Slash command metadata
18
+ └── MANIFEST_SCHEMA.md # This documentation
19
+ ```
20
+
21
+ ## Manifest Structure
22
+
23
+ ### Common Fields
24
+
25
+ Both manifests share these top-level fields:
26
+
27
+ | Field | Type | Purpose |
28
+ |-------|------|---------|
29
+ | `schema_version` | string | Manifest format version (e.g., "1.0.0") |
30
+ | `manifest_version` | string | Stravinsky package version this manifest was generated for |
31
+ | `description` | string | Brief description of manifest purpose |
32
+ | `generated_date` | ISO 8601 | Timestamp when manifest was created/updated |
33
+ | `schema` | object | Field definitions and their meanings |
34
+ | `[items]` | object | Collection of hooks or skills with metadata |
35
+ | `usage` | object | Integration notes for `update_manager.py` |
36
+
37
+ ### hooks_manifest.json Schema
38
+
39
+ Each hook entry has these fields:
40
+
41
+ ```json
42
+ {
43
+ "hook_name": {
44
+ "version": "0.2.63",
45
+ "source": "mcp_bridge/hooks/hook_name.py",
46
+ "description": "Brief description of hook purpose",
47
+ "hook_type": "PreToolUse|PostToolUse|UserPromptSubmit|Notification|SubagentStop|PreCompact|package|manager|session_idle|session_manager",
48
+ "checksum": "sha256_first_12_chars",
49
+ "lines_of_code": 150,
50
+ "updatable": true,
51
+ "priority": "critical|high|medium|low",
52
+ "required": true,
53
+ "dependencies": ["manager.py", "other_hook.py"]
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### skills_manifest.json Schema
59
+
60
+ Each skill entry has these fields:
61
+
62
+ ```json
63
+ {
64
+ "skill_name": {
65
+ "file_path": "strav.md or str/search.md",
66
+ "description": "Description from skill frontmatter",
67
+ "category": "core|research|implementation|architecture",
68
+ "checksum": "sha256_first_12_chars",
69
+ "lines_of_code": 200,
70
+ "updatable": true,
71
+ "priority": "critical|high|medium|low",
72
+ "agent_type": "explore|dewey|frontend|delphi|stravinsky|implementation_lead|code_reviewer",
73
+ "blocking": true,
74
+ "requires_auth": true,
75
+ "version_first_added": "0.1.0",
76
+ "notes": "Additional context or special considerations"
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Field Definitions
82
+
83
+ ### `version`
84
+ Semantic version of the hook/skill implementation. Format: `X.Y.Z` (e.g., `0.2.63`).
85
+
86
+ ### `source` / `file_path`
87
+ Absolute path (hooks) or relative path from `.claude/commands/` (skills).
88
+
89
+ ### `description`
90
+ One-line functional description of what the hook/skill does.
91
+
92
+ ### `hook_type` (hooks only)
93
+ Claude Code hook type that this hook implements:
94
+ - **PreToolUse**: Runs before tool execution (can block with exit code 2)
95
+ - **PostToolUse**: Runs after tool completes
96
+ - **UserPromptSubmit**: Runs when user submits prompt
97
+ - **Notification**: Runs on notification events
98
+ - **SubagentStop**: Runs when agent completes
99
+ - **PreCompact**: Runs before context compaction
100
+ - **package**: Module/package initialization
101
+ - **manager**: Hook management infrastructure
102
+ - **session_idle**: Session idle detection
103
+
104
+ ### `category` (skills only)
105
+ Skill category for organization:
106
+ - **core**: Essential orchestration features
107
+ - **research**: Documentation and code search
108
+ - **implementation**: Development workflows (test, review, deploy)
109
+ - **architecture**: Strategic advice and complex debugging
110
+
111
+ ### `checksum`
112
+ SHA-256 hash (first 12 characters) for integrity verification.
113
+
114
+ **How to verify/generate:**
115
+ ```bash
116
+ # Generate checksum
117
+ sha256sum mcp_bridge/hooks/hook_name.py | awk '{print substr($1,1,12)}'
118
+
119
+ # Verify file hasn't been modified
120
+ sha256sum -c <<< "checksum_value mcp_bridge/hooks/hook_name.py"
121
+ ```
122
+
123
+ ### `updatable`
124
+ - **true**: Official Stravinsky file - can be auto-updated by `update_manager.py`
125
+ - **false**: User customization or user-provided hook - skip during updates
126
+
127
+ **CRITICAL**: User hooks from `.claude/hooks/` should ALWAYS have `updatable: false` in the internal manifest comparison.
128
+
129
+ ### `priority`
130
+ Update urgency level:
131
+ - **critical**: Security fixes, core functionality - update immediately
132
+ - **high**: New features, important improvements - include in next release
133
+ - **medium**: Enhancements, can batch with other updates
134
+ - **low**: Optional improvements, can defer
135
+
136
+ ### `required`
137
+ - **true**: Hook is essential for core functionality and cannot be disabled
138
+ - **false**: Optional hook that provides enhanced behavior but isn't critical
139
+
140
+ ### `blocking` (skills only)
141
+ - **true**: Skill blocks execution until completion
142
+ - **false**: Skill runs asynchronously in background
143
+
144
+ ### `agent_type` (skills only)
145
+ Primary agent spawned by this skill:
146
+ - **stravinsky**: Task orchestration
147
+ - **explore**: Codebase search
148
+ - **dewey**: Documentation research
149
+ - **delphi**: Strategic architecture advisor
150
+ - **frontend**: UI/UX design
151
+ - **implementation_lead**: Coordinates implementation
152
+ - **code_reviewer**: Quality analysis
153
+
154
+ ### `requires_auth` (skills only)
155
+ - **true**: Skill requires OAuth setup (Gemini or OpenAI)
156
+ - **false**: Skill works without authentication
157
+
158
+ ### `dependencies`
159
+ List of other files this hook/skill depends on:
160
+ - Hooks typically depend on `manager.py`
161
+ - Some hooks depend on other hooks they coordinate with
162
+ - Skills may list dependent tools or agents
163
+
164
+ ## Integration with update_manager.py
165
+
166
+ The `update_manager.py` module should use these manifests to:
167
+
168
+ ### 1. Version Checking
169
+ ```python
170
+ # Load installed manifest
171
+ installed_manifest = load_manifest("hooks_manifest.json")
172
+
173
+ # Compare with remote version
174
+ if installed_manifest.version < remote_manifest.version:
175
+ # Updates available
176
+ pass
177
+ ```
178
+
179
+ ### 2. Integrity Verification
180
+ ```python
181
+ # Before updating, verify file hasn't been locally modified
182
+ current_checksum = compute_sha256(file_path)
183
+ expected_checksum = manifest[hook_name].checksum
184
+
185
+ if current_checksum != expected_checksum and manifest[hook_name].updatable:
186
+ # Local modifications detected - warn user or skip
187
+ skip_update(hook_name)
188
+ ```
189
+
190
+ ### 3. Selective Updates
191
+ ```python
192
+ # Only update files marked updatable=true
193
+ for hook_name, hook_info in manifest.items():
194
+ if hook_info.updatable and should_update(hook_info.priority):
195
+ update_hook(hook_name, new_version)
196
+ ```
197
+
198
+ ### 4. Dependency Resolution
199
+ ```python
200
+ # Ensure dependencies are met before updating
201
+ for dependency in hook_info.dependencies:
202
+ if not is_dependency_installed(dependency):
203
+ error("Missing dependency: " + dependency)
204
+ ```
205
+
206
+ ## Usage Examples
207
+
208
+ ### Checking Hook Status
209
+ ```bash
210
+ # Find all critical hooks needing updates
211
+ jq '.hooks | to_entries[] | select(.value.priority == "critical")' \
212
+ mcp_bridge/config/hooks_manifest.json
213
+
214
+ # Check if a hook is required
215
+ jq '.hooks.parallel_enforcer.required' \
216
+ mcp_bridge/config/hooks_manifest.json
217
+ ```
218
+
219
+ ### Verifying File Integrity
220
+ ```bash
221
+ # Verify all hooks match expected checksums
222
+ python -c "
223
+ import json
224
+ from pathlib import Path
225
+
226
+ with open('mcp_bridge/config/hooks_manifest.json') as f:
227
+ manifest = json.load(f)
228
+
229
+ for hook_name, info in manifest['hooks'].items():
230
+ file_path = info['source']
231
+ if Path(file_path).exists():
232
+ # Compute checksum and compare
233
+ pass
234
+ "
235
+ ```
236
+
237
+ ### Listing Available Skills
238
+ ```bash
239
+ # Extract all skills with their agents
240
+ jq '.skills | to_entries[] | {name: .key, agent: .value.agent_type, blocking: .value.blocking}' \
241
+ mcp_bridge/config/skills_manifest.json
242
+ ```
243
+
244
+ ## Best Practices
245
+
246
+ ### For Stravinsky Maintainers
247
+
248
+ 1. **Update on Release**: Regenerate manifests when releasing a new version
249
+ 2. **Verify Checksums**: Ensure all checksums are current and accurate
250
+ 3. **Document Changes**: Add notes to skills when updating functionality
251
+ 4. **Priority Assignment**: Use priority levels consistently across releases
252
+ 5. **Dependency Tracking**: Keep dependency lists current and accurate
253
+
254
+ ### For Package Users
255
+
256
+ 1. **Don't Modify Manifests**: Let `update_manager.py` handle manifest updates
257
+ 2. **Preserve Customizations**: Don't modify hooks marked as required=true without good reason
258
+ 3. **Check Auth Requirements**: Ensure OAuth is configured for skills requiring authentication
259
+ 4. **Monitor Critical Updates**: Subscribe to updates for hooks marked priority=critical
260
+
261
+ ### For Developers
262
+
263
+ 1. **Custom Hooks**: Create custom hooks in `.claude/hooks/` (not in package)
264
+ 2. **Hook Testing**: Always test hooks with sample input before deploying
265
+ 3. **Checksum Calculation**: Update checksums when modifying hook files
266
+ 4. **Dependency Management**: Clearly document all hook dependencies
267
+
268
+ ## Schema Evolution
269
+
270
+ ### Version 1.0.0 (Current)
271
+ - Initial manifest format
272
+ - Hook and skill metadata tracking
273
+ - Integrity verification via checksums
274
+ - Priority-based update strategy
275
+
276
+ ### Future Versions
277
+ - Will be tracked in `schema_version`
278
+ - Backward compatibility maintained where possible
279
+ - Migration guides provided for breaking changes
280
+
281
+ ## Troubleshooting
282
+
283
+ ### Checksum Mismatch
284
+ **Problem**: Hook has been modified locally
285
+ **Solution**:
286
+ - If intentional: Update manifest checksum or move to custom hooks
287
+ - If accidental: Restore original file from package
288
+
289
+ ### Missing Dependencies
290
+ **Problem**: Hook dependency is not installed
291
+ **Solution**: Ensure all required hooks in `dependencies` list are installed
292
+
293
+ ### Update Failures
294
+ **Problem**: `update_manager.py` fails to update a hook
295
+ **Solution**:
296
+ - Check file permissions
297
+ - Verify checksum hasn't changed unexpectedly
298
+ - Check for read-only files
299
+
300
+ ## Related Files
301
+
302
+ - `mcp_bridge/cli/install_hooks.py` - Hook installation script
303
+ - `mcp_bridge/config/hooks.py` - Hook configuration utilities
304
+ - `mcp_bridge/hooks/manager.py` - Hook execution manager
305
+ - `.claude/settings.json` - Claude Code hook configuration