# Companion Agent Module Installation Contract

**Status**: Phase 4 — Module Auto-Installation Pipeline
**Date**: 2026-02-15
**Author**: Sonnet (XO)

## Overview

The companion agent (bare metal server management) must implement module installation support to enable automated plugin deployment from the Corrosion admin panel.

This document defines the NATS subject contract for module installation commands and responses.

---

## NATS Subject Pattern

### Command Subject
```
corrosion.{license_id}.cmd.module.install
```

### Response Subject
```
corrosion.{license_id}.module.install.result
```

---

## Request Payload

The backend publishes a JSON payload to the command subject when a module installation is triggered:

```json
{
  "module_id": "loot-manager",
  "download_url": "https://cdn.corrosionmgmt.com/modules/LootManager.cs",
  "filename": "LootManager.cs",
  "target_path": "oxide/plugins/"
}
```

### Fields

| Field          | Type   | Required | Description                                                          |
|----------------|--------|----------|----------------------------------------------------------------------|
| `module_id`    | string | Yes      | Module slug identifier (e.g., "loot-manager")                       |
| `download_url` | string | Yes      | Signed URL to download the plugin file (.cs file)                   |
| `filename`     | string | Yes      | Filename to save the plugin as (e.g., "LootManager.cs")             |
| `target_path`  | string | Yes      | Relative path from server root to install location (e.g., "oxide/plugins/") |

---

## Expected Agent Behavior

1. **Download Plugin File**
   - Make HTTP GET request to `download_url`
   - Validate response is successful (2xx status)
   - Read file contents into memory

2. **Install Plugin**
   - Navigate to `{server_root}/{target_path}`
   - Write file contents to `{target_path}/{filename}`
   - Ensure file permissions allow server process to read it

3. **Reload Plugins**
   - Execute console command: `oxide.reload *`
   - Wait for plugin to load
   - Verify plugin appears in loaded plugin list

4. **Publish Result**
   - Publish success/failure result to `corrosion.{license_id}.module.install.result`
   - Include error details if installation failed

---

## Response Payload

The agent must publish a JSON response to the result subject after installation completes (or fails):

### Success Response
```json
{
  "module_id": "loot-manager",
  "success": true,
  "error": null
}
```

### Failure Response
```json
{
  "module_id": "loot-manager",
  "success": false,
  "error": "Failed to download plugin file: connection timeout"
}
```

### Fields

| Field       | Type    | Required | Description                                              |
|-------------|---------|----------|----------------------------------------------------------|
| `module_id` | string  | Yes      | Echo of the module_id from the request                   |
| `success`   | boolean | Yes      | `true` if installation succeeded, `false` if it failed   |
| `error`     | string  | No       | Human-readable error message (required if success=false) |

---

## Error Handling

The agent should report failure for any of these conditions:

- **Download Failure**: HTTP request fails or returns non-2xx status
- **File Write Failure**: Unable to write plugin file to disk (permissions, disk full, path doesn't exist)
- **Plugin Load Failure**: Plugin file saved but failed to load when `oxide.reload *` was executed
- **Timeout**: Operation takes longer than 60 seconds

The backend will timeout after 60 seconds if no response is received and mark the installation as "failed" in the database.

---

## Implementation Notes

- The backend uses NATS request/reply pattern with a 60-second timeout
- Plugin files are small (<1MB typically), so download should be fast
- The agent should verify `oxide/plugins/` directory exists before attempting write
- If the plugin already exists, overwrite it (this is an update/reinstall scenario)
- The agent does NOT need to check if the module is purchased — backend already verified this

---

## Example Implementation (Pseudocode)

```go
func HandleModuleInstall(msg *nats.Msg) {
    var cmd ModuleInstallCommand
    json.Unmarshal(msg.Data, &cmd)

    // Download plugin file
    resp, err := http.Get(cmd.DownloadURL)
    if err != nil {
        publishResult(msg.Reply, cmd.ModuleID, false, err.Error())
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        publishResult(msg.Reply, cmd.ModuleID, false, "Download failed: " + resp.Status)
        return
    }

    pluginData, _ := ioutil.ReadAll(resp.Body)

    // Write to disk
    targetFile := filepath.Join(serverRoot, cmd.TargetPath, cmd.Filename)
    err = ioutil.WriteFile(targetFile, pluginData, 0644)
    if err != nil {
        publishResult(msg.Reply, cmd.ModuleID, false, "File write failed: " + err.Error())
        return
    }

    // Reload plugins
    err = sendConsoleCommand("oxide.reload *")
    if err != nil {
        publishResult(msg.Reply, cmd.ModuleID, false, "Plugin reload failed: " + err.Error())
        return
    }

    // Success
    publishResult(msg.Reply, cmd.ModuleID, true, "")
}

func publishResult(subject, moduleID string, success bool, errorMsg string) {
    result := ModuleInstallResult{
        ModuleID: moduleID,
        Success:  success,
        Error:    errorMsg,
    }
    data, _ := json.Marshal(result)
    natsClient.Publish(subject, data)
}
```

---

## Testing

### Manual Test Procedure

1. Purchase a module from the Corrosion dashboard (e.g., Loot Manager)
2. Click "Install" button on the module card
3. Monitor NATS subject: `corrosion.{your_license_id}.cmd.module.install`
4. Verify agent receives command payload
5. Verify agent downloads plugin file
6. Verify agent writes file to `oxide/plugins/LootManager.cs`
7. Verify agent executes `oxide.reload *`
8. Verify agent publishes success result
9. Refresh dashboard — module status should show "installed"

### Failure Scenarios to Test

- Invalid download URL (404) → agent reports "Download failed"
- Disk full → agent reports "File write failed"
- Plugin syntax error → agent reports "Plugin load failed"
- No response from agent → backend times out after 60s, marks "failed"

---

## Related Files

- **Backend Service**: `backend/src/services/module_installer.rs`
- **API Endpoint**: `backend/src/api/modules.rs` (POST `/api/modules/install`)
- **Database Schema**: `backend/migrations/009_module_licensing.sql`
- **Companion Agent Repo**: TBD (Go implementation)

---

## Status

- [x] Backend service implemented (`ModuleInstaller`)
- [x] API endpoint wired (`POST /api/modules/install`)
- [x] NATS contract documented
- [ ] Companion agent implementation (Go)
- [ ] End-to-end testing

---

**Next Steps**: Implement this contract in the companion agent codebase (Go).
