---
phase: 05-secrets-management
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/clawrium/core/secrets.py
  - src/clawrium/platform/registry/openclaw/manifest.yaml
  - tests/test_secrets.py
autonomous: true
requirements: [SEC-01, SEC-03]
must_haves:
  truths:
    - "Secrets stored in secrets.json with mode 600 permissions"
    - "Secrets have metadata: key, value, created_at, updated_at, description"
    - "File locking prevents concurrent access corruption"
    - "Manifest declares required_secrets and optional_secrets fields"
  artifacts:
    - path: "src/clawrium/core/secrets.py"
      provides: "Secret storage with CRUD operations and file locking"
      exports: ["load_secrets", "save_secrets", "get_secret", "set_secret", "remove_secret", "list_secrets", "SECRETS_FILE", "SecretEntry", "SecretsFileCorruptedError", "DuplicateSecretError"]
    - path: "src/clawrium/platform/registry/openclaw/manifest.yaml"
      provides: "Manifest with required_secrets and optional_secrets fields"
      contains: "required_secrets"
    - path: "tests/test_secrets.py"
      provides: "Unit tests for secrets module"
      min_lines: 100
  key_links:
    - from: "src/clawrium/core/secrets.py"
      to: "src/clawrium/core/config.py"
      via: "get_config_dir(), init_config_dir()"
      pattern: "from clawrium\\.core\\.config import"
---

<objective>
Create core secrets storage module with file locking and extend manifest schema with secrets requirements.

Purpose: Establish the foundation for secure secret storage (SEC-01, SEC-03) using the same patterns as hosts.py (fcntl locking, atomic writes, mode 600 permissions).

Output:
- `src/clawrium/core/secrets.py` — Secret storage with CRUD operations
- `src/clawrium/platform/registry/openclaw/manifest.yaml` — Extended with required_secrets/optional_secrets
- `tests/test_secrets.py` — Unit tests for secrets module
</objective>

<execution_context>
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/workflows/execute-plan.md
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-secrets-management/05-CONTEXT.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
<!-- Executor should use these directly -- no codebase exploration needed. -->

From src/clawrium/core/config.py:
```python
def get_config_dir() -> Path:
    """Get the Clawrium configuration directory path."""

def init_config_dir() -> Path:
    """Create and return the configuration directory with 0700 permissions."""
```

From src/clawrium/core/hosts.py (patterns to replicate):
```python
HOSTS_FILE = "hosts.json"

class HostsFileCorruptedError(Exception):
    """Raised when hosts.json cannot be parsed."""
    pass

@contextmanager
def _hosts_lock():
    """Context manager for exclusive access using fcntl.flock."""
    config_dir = init_config_dir()
    lock_path = config_dir / ".hosts.lock"
    lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o600)
    try:
        fcntl.flock(lock_fd, fcntl.LOCK_EX)
        yield
    finally:
        fcntl.flock(lock_fd, fcntl.LOCK_UN)
        os.close(lock_fd)

def save_hosts(hosts: list[dict]) -> None:
    """Save atomically with temp file + rename, mode 0o600."""
    config_dir = init_config_dir()
    with _hosts_lock():
        fd, tmp_path = tempfile.mkstemp(dir=config_dir, suffix=".tmp")
        try:
            os.fchmod(fd, 0o600)
            with os.fdopen(fd, "w") as f:
                json.dump(hosts, f, indent=2)
            os.replace(tmp_path, hosts_path)
        except Exception:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
            raise
```

From src/clawrium/platform/registry/openclaw/manifest.yaml:
```yaml
name: openclaw
description: "Open-source AI assistant framework"
entries:
  - version: "0.1.0"
    os: ubuntu
    os_version: "24.04"
    arch: x86_64
    requirements:
      min_memory_mb: 2048
      gpu_required: false
      dependencies:
        nodejs: ">=20.0.0"
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create secrets storage module</name>
  <files>src/clawrium/core/secrets.py, tests/test_secrets.py</files>
  <read_first>
    - src/clawrium/core/hosts.py (source of truth for file locking and atomic write patterns)
    - src/clawrium/core/config.py (get_config_dir, init_config_dir)
    - tests/test_hosts.py (test patterns to replicate)
    - tests/conftest.py (fixtures: tmp_config_dir, isolated_config)
  </read_first>
  <behavior>
    - Test: load_secrets() with no file returns empty dict
    - Test: load_secrets() with valid JSON returns dict[str, SecretEntry]
    - Test: load_secrets() with invalid JSON raises SecretsFileCorruptedError
    - Test: save_secrets() creates file with mode 0o600
    - Test: save_secrets() creates config dir if missing
    - Test: set_secret(key, value, description) creates entry with created_at, updated_at
    - Test: set_secret(key, value) on existing key updates updated_at, preserves created_at
    - Test: get_secret(key) returns SecretEntry or None
    - Test: remove_secret(key) removes entry, returns True; returns False if not found
    - Test: list_secrets() returns list of keys (not values)
  </behavior>
  <action>
Create `src/clawrium/core/secrets.py` with:

1. **Constants and types** (per D-02, D-03):
```python
SECRETS_FILE = "secrets.json"

class SecretEntry(TypedDict):
    key: str
    value: str
    created_at: str  # ISO 8601 timestamp
    updated_at: str  # ISO 8601 timestamp
    description: str  # Optional description, empty string if not provided
```

2. **Exceptions**:
```python
class SecretsFileCorruptedError(Exception):
    """Raised when secrets.json cannot be parsed."""
    pass

class DuplicateSecretError(Exception):
    """Raised when trying to add a secret that already exists (for strict mode)."""
    pass
```

3. **Lock context manager** (per D-04, replicate `_hosts_lock` pattern):
```python
@contextmanager
def _secrets_lock():
    """Context manager for exclusive access to secrets.json using fcntl.flock."""
    config_dir = init_config_dir()
    lock_path = config_dir / ".secrets.lock"
    lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o600)
    try:
        fcntl.flock(lock_fd, fcntl.LOCK_EX)
        yield
    finally:
        fcntl.flock(lock_fd, fcntl.LOCK_UN)
        os.close(lock_fd)
```

4. **CRUD operations**:
- `load_secrets() -> dict[str, SecretEntry]`: Load from secrets.json, return empty dict if file missing, raise SecretsFileCorruptedError on invalid JSON
- `save_secrets(secrets: dict[str, SecretEntry]) -> None`: Atomic write with temp file + rename, mode 0o600, using _secrets_lock
- `get_secret(key: str) -> SecretEntry | None`: Return secret entry or None
- `set_secret(key: str, value: str, description: str = "") -> bool`: Set/update secret, return True if created new, False if updated existing. Use ISO 8601 timestamps.
- `remove_secret(key: str) -> bool`: Remove by key, return True if found, False otherwise
- `list_secrets() -> list[str]`: Return sorted list of secret keys (not values per D-11)

5. **Module exports** in `__all__`:
```python
__all__ = [
    "load_secrets",
    "save_secrets",
    "get_secret",
    "set_secret",
    "remove_secret",
    "list_secrets",
    "SECRETS_FILE",
    "SecretEntry",
    "SecretsFileCorruptedError",
    "DuplicateSecretError",
]
```

Create `tests/test_secrets.py` with tests for all behaviors listed above. Use `isolated_config` fixture from conftest.py.
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && make test PYTEST_ARGS="-x tests/test_secrets.py -v"</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/core/secrets.py exists and contains `SECRETS_FILE = "secrets.json"`
    - src/clawrium/core/secrets.py contains `class SecretEntry(TypedDict):`
    - src/clawrium/core/secrets.py contains `def _secrets_lock():`
    - src/clawrium/core/secrets.py contains `def load_secrets() -> dict[str, SecretEntry]:`
    - src/clawrium/core/secrets.py contains `def save_secrets(`
    - src/clawrium/core/secrets.py contains `def set_secret(`
    - src/clawrium/core/secrets.py contains `def get_secret(`
    - src/clawrium/core/secrets.py contains `def remove_secret(`
    - src/clawrium/core/secrets.py contains `def list_secrets(`
    - src/clawrium/core/secrets.py contains `os.fchmod(fd, 0o600)`
    - src/clawrium/core/secrets.py contains `fcntl.flock(`
    - tests/test_secrets.py contains `def test_load_secrets_no_file`
    - tests/test_secrets.py contains `def test_save_secrets_file_permissions`
    - tests/test_secrets.py contains `def test_set_secret_creates_entry`
    - `make test PYTEST_ARGS="-x tests/test_secrets.py"` exits 0
  </acceptance_criteria>
  <done>
    - secrets.py implements all CRUD operations with fcntl locking
    - File permissions set to 0o600 on save
    - All tests pass
  </done>
</task>

<task type="auto">
  <name>Task 2: Extend manifest schema with secrets requirements</name>
  <files>src/clawrium/platform/registry/openclaw/manifest.yaml, src/clawrium/core/registry.py</files>
  <read_first>
    - src/clawrium/platform/registry/openclaw/manifest.yaml (current manifest structure)
    - src/clawrium/core/registry.py (ManifestEntry TypedDict to extend)
  </read_first>
  <action>
1. **Update manifest.yaml** (per D-06, D-07):
Add top-level `required_secrets` and `optional_secrets` fields to `src/clawrium/platform/registry/openclaw/manifest.yaml`:

```yaml
name: openclaw
description: "Open-source AI assistant framework"

# Secrets required for this claw (install will fail if missing)
required_secrets:
  - key: OPENAI_API_KEY
    description: "OpenAI API key for LLM requests"

# Optional secrets (claw works without them but with reduced functionality)
optional_secrets:
  - key: ANTHROPIC_API_KEY
    description: "Anthropic API key for Claude models"

entries:
  - version: "0.1.0"
    os: ubuntu
    # ... (keep existing entries unchanged)
```

2. **Extend registry.py types**:
Add new TypedDicts and extend ClawManifest:

```python
class SecretDefinition(TypedDict):
    """Secret definition in manifest."""
    key: str
    description: str

class ClawManifest(TypedDict):
    """Complete claw manifest with all platform entries."""
    name: str
    description: str
    entries: list[ManifestEntry]
    required_secrets: NotRequired[list[SecretDefinition]]  # Add this
    optional_secrets: NotRequired[list[SecretDefinition]]  # Add this
```

3. **Add helper function** to get required secrets for validation:
```python
def get_required_secrets(claw_name: str) -> list[SecretDefinition]:
    """Get list of required secrets for a claw.

    Returns empty list if claw has no required_secrets field.
    """
    manifest = load_manifest(claw_name)
    return manifest.get("required_secrets", [])

def get_optional_secrets(claw_name: str) -> list[SecretDefinition]:
    """Get list of optional secrets for a claw.

    Returns empty list if claw has no optional_secrets field.
    """
    manifest = load_manifest(claw_name)
    return manifest.get("optional_secrets", [])
```

4. **Update __all__ exports** in registry.py to include new functions and types.
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && python -c "from clawrium.core.registry import load_manifest, get_required_secrets, SecretDefinition; m = load_manifest('openclaw'); print('required_secrets' in m); print(get_required_secrets('openclaw'))" && make test PYTEST_ARGS="-x tests/test_registry.py -v"</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/platform/registry/openclaw/manifest.yaml contains `required_secrets:`
    - src/clawrium/platform/registry/openclaw/manifest.yaml contains `key: OPENAI_API_KEY`
    - src/clawrium/platform/registry/openclaw/manifest.yaml contains `optional_secrets:`
    - src/clawrium/core/registry.py contains `class SecretDefinition(TypedDict):`
    - src/clawrium/core/registry.py contains `def get_required_secrets(`
    - src/clawrium/core/registry.py contains `def get_optional_secrets(`
    - `python -c "from clawrium.core.registry import get_required_secrets; print(get_required_secrets('openclaw'))"` outputs list with OPENAI_API_KEY entry
    - `make test PYTEST_ARGS="-x tests/test_registry.py"` exits 0
  </acceptance_criteria>
  <done>
    - manifest.yaml includes required_secrets and optional_secrets fields
    - registry.py exports SecretDefinition type and getter functions
    - Existing registry tests still pass
  </done>
</task>

</tasks>

<verification>
After completing all tasks:

1. Verify secrets module structure:
```bash
grep -n "def load_secrets\|def save_secrets\|def set_secret\|def get_secret\|def remove_secret\|def list_secrets" src/clawrium/core/secrets.py
```

2. Verify file permission enforcement:
```bash
grep -n "0o600" src/clawrium/core/secrets.py
```

3. Verify manifest extension:
```bash
grep -n "required_secrets\|optional_secrets" src/clawrium/platform/registry/openclaw/manifest.yaml
```

4. Run all tests:
```bash
make test
```
</verification>

<success_criteria>
- secrets.py provides CRUD operations with atomic writes and fcntl locking
- File permissions enforced at 0o600 on every save
- Manifest schema extended with required_secrets and optional_secrets
- All existing tests pass, new secrets tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/05-secrets-management/05-01-SUMMARY.md`
</output>
