---
phase: 06-make-secrets-specific-to-installed-claws-not-global
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/clawrium/core/secrets.py
  - tests/test_secrets.py
autonomous: true
requirements:
  - PSEC-01
  - PSEC-05

must_haves:
  truths:
    - "Secrets are stored per claw instance, not globally"
    - "Instance identity is host:claw_type:claw_name format"
    - "Same secret key can exist with different values per instance"
    - "Secrets can only be set for installed claws"
  artifacts:
    - path: "src/clawrium/core/secrets.py"
      provides: "Per-instance secret storage with nested JSON structure"
      exports: ["load_secrets", "save_secrets", "get_instance_secrets", "set_instance_secret", "remove_instance_secret", "list_instances_with_secrets", "get_instance_key"]
    - path: "tests/test_secrets.py"
      provides: "Tests for per-instance secret operations"
      contains: "test_set_instance_secret"
  key_links:
    - from: "src/clawrium/core/secrets.py"
      to: "secrets.json"
      via: "nested dict with instance keys"
      pattern: "host:claw_type:claw_name"
---

<objective>
Refactor secrets module from global to per-claw-instance scoping.

Purpose: Each installed claw instance needs its own secrets (e.g., different OPENAI_API_KEY per claw), enabling fleet-wide secret management where the same key can have different values per instance.

Output: Updated core/secrets.py with nested JSON storage structure and instance-scoped operations.
</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/06-make-secrets-specific-to-installed-claws-not-global/06-CONTEXT.md

<interfaces>
<!-- Key types and contracts from existing codebase -->

From src/clawrium/core/secrets.py (current global implementation to refactor):
```python
class SecretEntry(TypedDict):
    key: str
    value: str
    created_at: str  # ISO 8601 timestamp
    updated_at: str  # ISO 8601 timestamp
    description: str

def load_secrets() -> dict[str, SecretEntry]: ...
def save_secrets(secrets: dict[str, SecretEntry]) -> None: ...
def get_secret(key: str) -> SecretEntry | None: ...
def set_secret(key: str, value: str, description: str = "", *, strict: bool = False) -> bool: ...
def remove_secret(key: str) -> bool: ...
def list_secrets() -> list[str]: ...
def validate_secret_key(key: str) -> str: ...
```

From src/clawrium/core/hosts.py (for getting installed claws):
```python
def load_hosts() -> list[dict]: ...
def get_host(identifier: str) -> dict | None: ...
# Host dict has: {"hostname": str, "claws": {"openclaw": {"status": "installed", "user": str, ...}}, ...}
```

Storage format per D-07, D-08, D-09:
```json
{
  "wolf:openclaw:work": {
    "OPENAI_API_KEY": {"key": "OPENAI_API_KEY", "value": "...", "created_at": "...", "updated_at": "...", "description": "..."}
  },
  "wolf:openclaw:personal": {
    "OPENAI_API_KEY": {"key": "OPENAI_API_KEY", "value": "...", ...}
  }
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Refactor secrets module for per-instance storage</name>
  <files>src/clawrium/core/secrets.py, tests/test_secrets.py</files>
  <read_first>
    - src/clawrium/core/secrets.py (current implementation to refactor)
    - src/clawrium/core/hosts.py (for get_host, load_hosts patterns)
    - tests/test_secrets.py (existing test patterns to update)
  </read_first>
  <behavior>
    - Test: get_instance_key("wolf", "openclaw", "work") returns "wolf:openclaw:work"
    - Test: set_instance_secret("wolf:openclaw:work", "OPENAI_API_KEY", "sk-123") creates nested entry
    - Test: get_instance_secrets("wolf:openclaw:work") returns dict of SecretEntry for that instance
    - Test: set same key on different instances stores different values
    - Test: remove_instance_secret removes only from specified instance
    - Test: list_instances_with_secrets returns list of instance keys that have secrets
    - Test: get_installed_claw raises error if claw not installed (validates against hosts.json)
    - Test: file permissions remain 0o600
  </behavior>
  <action>
    1. Add new function `get_instance_key(host: str, claw_type: str, claw_name: str) -> str`:
       - Returns f"{host}:{claw_type}:{claw_name}" per D-08

    2. Add new function `get_installed_claw(claw_name: str) -> tuple[str, str, str]`:
       - Search all hosts in load_hosts() for a claw with matching name (from claws dict)
       - Claw name is in host["claws"][claw_type]["user"] which follows pattern prefix-clawname
       - Actually per D-13, lookup by claw_name which is unique across fleet
       - For each host, check each claw_type in host.get("claws", {})
       - The claw "name" is stored in the "user" field minus the prefix (e.g., "opc-work" -> "work")
       - Raise ClawNotFoundError if no installed claw matches
       - Return (hostname, claw_type, claw_name)

    3. Add class `ClawNotFoundError(Exception)` for when claw name not found

    4. Modify `load_secrets() -> dict[str, dict[str, SecretEntry]]`:
       - Change return type from dict[str, SecretEntry] to dict[str, dict[str, SecretEntry]]
       - Top-level keys are instance keys (host:claw_type:claw_name)
       - Second-level keys are secret keys (OPENAI_API_KEY, etc.)
       - Delete old secrets.json on first access per D-06 (clean break)

    5. Keep `save_secrets(secrets: dict[str, dict[str, SecretEntry]])`:
       - Same atomic write pattern with fcntl locking
       - File permissions 0o600

    6. Add `get_instance_secrets(instance_key: str) -> dict[str, SecretEntry]`:
       - Load all secrets, return secrets.get(instance_key, {})

    7. Add `set_instance_secret(instance_key: str, key: str, value: str, description: str = "") -> bool`:
       - Validate key with validate_secret_key()
       - Load secrets, create instance dict if not exists
       - Set or update SecretEntry at secrets[instance_key][key]
       - Return True if created, False if updated

    8. Add `remove_instance_secret(instance_key: str, key: str) -> bool`:
       - Validate key with validate_secret_key()
       - Load secrets, remove key from instance dict if exists
       - Return True if removed, False if not found

    9. Add `list_instances_with_secrets() -> list[str]`:
       - Return sorted list of instance keys that have at least one secret

    10. Keep old functions but deprecate (mark for removal):
        - Keep get_secret, set_secret, remove_secret, list_secrets as stubs that raise DeprecatedError
        - This ensures any lingering references fail fast

    11. Update __all__ to export new functions:
        - Add: get_instance_key, get_installed_claw, get_instance_secrets, set_instance_secret, remove_instance_secret, list_instances_with_secrets, ClawNotFoundError
        - Keep: load_secrets, save_secrets, validate_secret_key, SECRETS_FILE, SecretEntry, SecretsFileCorruptedError, InvalidSecretKeyError

    12. Update tests/test_secrets.py:
        - Update existing tests to use new nested structure
        - Add tests for get_instance_key
        - Add tests for instance-scoped set/get/remove/list
        - Add test for same key different values per instance
        - Add test for ClawNotFoundError when claw not installed
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && make test</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/core/secrets.py contains `def get_instance_key(`
    - src/clawrium/core/secrets.py contains `def get_instance_secrets(`
    - src/clawrium/core/secrets.py contains `def set_instance_secret(`
    - src/clawrium/core/secrets.py contains `def remove_instance_secret(`
    - src/clawrium/core/secrets.py contains `def list_instances_with_secrets(`
    - src/clawrium/core/secrets.py contains `class ClawNotFoundError`
    - tests/test_secrets.py contains `test_get_instance_key`
    - tests/test_secrets.py contains `test_set_instance_secret`
    - tests/test_secrets.py contains `test_same_key_different_instances`
    - `make test` exits with code 0
  </acceptance_criteria>
  <done>
    - Per-instance secret storage functions implemented
    - Instance key format is host:claw_type:claw_name
    - Same secret key stores different values per instance
    - Old global functions deprecated
    - All tests pass
  </done>
</task>

</tasks>

<verification>
```bash
cd /home/devashish/workspace/ric03uec/clawrium
make test
make lint
```
</verification>

<success_criteria>
- Per-instance secret storage implemented with nested JSON structure
- Instance key format: host:claw_type:claw_name
- Same key can have different values per instance
- File permissions remain 0o600
- All tests pass
- Code passes linting
</success_criteria>

<output>
After completion, create `.planning/phases/06-make-secrets-specific-to-installed-claws-not-global/06-01-SUMMARY.md`
</output>
