Files
meshcentral-openclaw-bridge/review/REVIEW.md

492 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Review Report: MeshCentral ↔ OpenClaw Bridge
**Review Date:** 2026-08-24
**Reviewer:** Independent subagent (clawdia review flow)
**Scope:** Implementation under `/home/ledadmin/projects/meshcentral-openclaw-bridge/implementation/`
**Reference Docs:** PROJECT.md, SECURITY.md, research/INTEGRATION-RESEARCH.md
---
## Executive Summary
The implementation provides a well-structured scaffold for the MeshCentral ↔ OpenClaw bridge with solid contracts, typed schemas via Zod, and comprehensive mock-based testing. However, **the critical "real" adapter is completely unimplemented**, leaving only the mock path operational. The current codebase has three categories of issues:
1. **Critical:** Unimplemented real adapter (`MeshCentralRealAdapter`) with dangerous defaults (insecure TLS, no auth).
2. **High:** Test failures indicate missing error message alignment; linting configuration incomplete.
3. **Medium:** Diagnostic output sanitization and prompt-injection protection not addressed.
**Test Results:**
- 50 tests pass
- 2 tests fail (expectation mismatch on error message string, not logic)
- `npm run build` fails with TypeScript errors in test setup
- `npm run lint` fails due to missing ESLint configuration
**Recommendation:** Do **not** proceed with live MeshCentral integration until the real adapter is implemented according to security requirements and all test/lint failures are resolved.
---
## Severity-Ranked Findings
### 🔴 CRITICAL: Real adapter unimplemented with dangerous TLS defaults
**Location:** `src/adapter.ts:35-48`, `healthCheck()` method
**Severity:** Critical
The `MeshCentralRealAdapter.healthCheck()` method uses raw `fetch` with no TLS validation options:
```typescript
async healthCheck(): Promise<boolean> {
try {
const response = await fetch(`${this.config.meshCentralUrl}/api`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
return response.ok;
} catch {
return false;
}
}
```
**Issues:**
- No certificate verification (relies on Node.js default, which may not validate against internal CA).
- `fetch` does not support WebSocket connections; MeshCentral's control API uses `wss://.../control.ashx`, not `/api`.
- Research found upstream `meshctrl.js` uses `rejectUnauthorized: false`—this should be explicitly avoided.
- No support for meshctrl-style WebSocket authentication (`x-meshauth` header with base64-encoded credentials).
**Attack Surface:** MITM attack, credential theft via insecure WebSocket upgrade.
**Required Fix:**
- Replace `fetch` with a proper WebSocket client (`ws` package, already in dependencies).
- Enable TLS certificate validation (do **not** disable it).
- Support custom CA bundle for internal MeshCentral deployments.
- Implement meshctrl protocol handshake (research confirms `wss://host:port/control.ashx`).
---
### 🔴 CRITICAL: No authentication or authorization implementation
**Location:** `src/adapter.ts` (entire file)
**Severity:** Critical
The research document explicitly identifies two authentication modes:
1. Username/password via `x-meshauth` WebSocket header (base64 encoded).
2. Server login key (highly sensitive—should not be used for the bridge).
**Current State:** The real adapter has no authentication mechanism. It does not:
- Accept or validate credentials.
- Support login keys (even in config).
- Manage WebSocket sessions or reconnects.
**Required Fix:**
- Add `username`, `password` (or `loginKey`) to `BridgeConfig`.
- Implement `x-meshauth` header encoding per upstream `meshctrl.js` source.
- Store secrets in environment or protected file with restrictive permissions (0600).
- Fail closed if credentials are missing or invalid.
---
### 🟠 HIGH: Test expectation mismatch (non-blocking but indicates incomplete validation)
**Location:** `src/__tests__/mock-adapter.test.ts:56,86`
**Severity:** High
Tests expect error message substring `"Resource not found"` but actual errors use more specific messages:
| Test | Expected | Actual |
|------|----------|--------|
| `getDevice` non-existent | `"Resource not found"` | `"Device not found: nonexistent"` |
| `getGroup` non-existent | `"Resource not found"` | `"Group not found: nonexistent"` |
**Fix:** Update test expectations to match the current error messages, or standardize error messages across the codebase.
```typescript
// Recommendation: Use consistent prefix for all resource-not-found errors
`Resource not found: device ${deviceId}`
```
---
### 🟠 HIGH: TypeScript build fails in test setup
**Location:** `src/__tests__/setup.ts`
**Severity:** High
Build errors prevent compilation and CI validation:
```
src/__tests__/setup.ts(13,15): error TS2428: All declarations of 'Expect' must have identical type parameters.
src/__tests__/setup.ts(23,13): error TS7006: Parameter 'received' implicitly has an 'any' type.
```
**Root Cause:** Conflicting `jest` and `jest-extended` types with mismatched generic parameters. The custom matchers declare `Expect<T>` without the required type argument.
**Required Fix:**
- Align custom matcher types with `jest` v29 expectations.
- Remove duplicate import of `'jest-extended'`.
- Use proper typing for `received` parameter.
---
### 🟠 HIGH: ESLint configuration missing
**Location:** Project root
**Severity:** High
Lint command fails:
```
ESLint couldn't find a configuration file.
```
**Required Fix:**
- Add `.eslintrc.yml` or `eslint.config.js`.
- Enforce TypeScript rules (`@typescript-eslint/`).
- Configure import ordering, unused variables, and strict null checks.
Example minimal config (`eslint.config.js`):
```javascript
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/strict-boolean-expressions': 'off',
},
};
```
---
### 🟠 HIGH: Diagnostics produce raw output without sanitization
**Location:** `src/mock-adapter.ts:54-103`, `DIAGNOSTIC_OUTPUTS`
**Severity:** High
Diagnostic output is hard-coded static strings. Research document warns:
- Endpoint command output may contain secrets (passwords, keys).
- Device names, logs, and process lists can expose personal data.
- Prompt injection via untrusted output is a valid threat.
**Current State:** No sanitization or escape of special characters, secrets, or prompt-injection vectors.
**Required Fix:**
- Implement output sanitization: replace known secret patterns (password=, token=, api_key=).
- Truncate or mask sensitive fields in logs.
- Add a `sanitizeOutput()` function before returning results.
- Consider adding an explicit "safe output only" mode.
---
### 🟡 MEDIUM: No device/group allowlists
**Location:** `src/adapter.ts`, `contracts.ts`
**Severity:** Medium
Research document explicitly requires:
> Restrict targets using explicit device/group allowlists.
**Current State:**
- Configuration has no `allowedDeviceIds` or `allowedGroupIds`.
- No validation that requested devices/groups are in an allowlist.
**Required Fix:**
- Add `allowedDeviceIds?: string[]` and `allowedGroupIds?: string[]` to `BridgeConfig`.
- Add validation in adapter methods: reject if target not in allowlist (or empty allowlist = all allowed).
- Audit log must record whether allowlist check passed.
---
### 🟡 MEDIUM: No audit logging
**Location:** `src/` (all files)
**Severity:** Medium
Security requirements mandate:
> Log authentication events, requests, target, operation, confirmation, result, and timestamp.
**Current State:** No logging implementation. Debug logs or console.log statements are absent.
**Required Fix:**
- Introduce `BridgeLogger` interface with methods:
- `logAuth(principal, success, details)`
- `logRequest(requestId, principal, operation, target, timestamp)`
- `logResult(requestId, success, details)`
- Use a structured formatter (JSON) for downstream processing.
- Never log secrets (passwords, keys, tokens).
---
### 🟡 MEDIUM: No connection resilience
**Location:** `src/adapter.ts`
**Severity:** Medium
Research document notes:
> Whether sessions expire, how reconnects behave, and whether a reconnect can replay an action.
**Current State:** No retry logic, backoff, or session management.
**Required Fix:**
- Add `maxRetries`, `retryBackoffMs` to config.
- Implement exponential backoff on WebSocket connect failures.
- Add reconnect guard to prevent duplicate action replay (e.g., request ID deduplication).
---
### 🟡 MEDIUM: No version compatibility checking
**Location:** `src/adapter.ts`, ` contracts.ts`
**Severity:** Medium
Research document warns:
> The bridge should record the tested server version and client/source commit, use semver/commit compatibility checks, and fail closed or enter degraded read-only mode when an unknown version is detected.
**Current State:** No MeshCentral version check.
**Required Fix:**
- Add `serverVersion` field to adapter metadata.
- Fetch version on connect (e.g., via `ServerInfo` control message).
- Define supported version range; fail closed if incompatible.
---
### 🟡 MEDIUM: Diagnostics allow all check types without policy
**Location:** `src/contracts.ts`, `mock-adapter.ts`
**Severity:** Medium
Research document requires:
> Define an allowlisted set of diagnostic checks... Restrict execution by device/group allowlist and command policy.
**Current State:**
- All `DiagnosticCheckType` values are enabled by default.
- No policy mapping check types to permitted device groups.
**Required Fix:**
- Add `diagnosticPolicy` config with allowed check types per group/device.
- Example:
```typescript
diagnosticPolicy: {
'grp-servers': ['diskSpace', 'uptime'],
'*': [],
}
```
- Reject diagnostic requests not in policy.
---
### 🟡 MEDIUM: No timeout enforcement on real adapter
**Location:** `src/adapter.ts`
**Severity:** Medium
The `healthCheck()` method has no timeout; `fetch` will hang indefinitely by default.
**Required Fix:**
- Use an AbortController with a configurable timeout.
- Reject with `ErrorCodes.TimeoutExceeded` if exceeded.
---
### 🟢 LOW: Diagnostic output truncation inconsistent
**Location:** `src/mock-adapter.ts`
**Severity:** Low
Truncation is simulated but not tested for edge cases:
- What happens if `maxOutputLength` is 0?
- What happens if output is exactly at the limit?
**Test Gap:** `timeout.test.ts` has no test for zero-length max output.
---
## Verification Results
### Test Execution (`npm test -- --coverage`)
```
Test Suites: 1 failed, 7 passed, 8 total
Tests: 2 failed, 50 passed, 52 total
Coverage: 92.3% statements, 91.66% branches, 72.72% functions
```
**Passing suites:**
- `contracts.test.ts` — All Zod schema validations pass.
- `adapter-factory.test.ts` — Factory correctly returns mock by default.
- `group-filtering.test.ts` — Device filtering by group works.
- `timeout.test.ts` — Timeout and output limits respected (except edge cases).
- `device-status.test.ts` — Status enum handling correct.
- `mock-data-loader.test.ts` — JSON fixture loading works.
- `integration.test.ts` — Full workflow mocks succeed.
**Failing tests (non-blocking, expectation issue):**
- `mock-adapter.test.ts` — 2 tests fail due to error message mismatch (not logic):
- `getDevice` non-existent expects "Resource not found" but receives "Device not found: nonexistent"
- `getGroup` non-existent expects "Resource not found" but receives "Group not found: nonexistent"
---
### Build (`npm run build`)
```
node_modules/@types/jest/index.d.ts(14,23): error TS2314
node_modules/@types/jest/index.d.ts(683,15): error TS2428
src/__tests__/setup.ts(13,15): error TS2428
src/__tests__/setup.ts(23,13): error TS7006
src/__tests__/setup.ts(30,14): error TS7006
src/__tests__/setup.ts(37,14): error TS7006
```
**Status:** Build fails—cannot produce dist artifacts. All errors are in test setup, not source code.
---
### Lint (`npm run lint`)
```
ESLint couldn't find a configuration file. To set up a configuration file for this project, please run:
npm init @eslint/config
```
**Status:** Linting entirely skipped—no code style enforcement. Missing `.eslintrc.*` or `eslint.config.js`.
---
### Code Coverage
```
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 92.3 | 91.66 | 72.72 | 91.83 |
src | 91.83 | 91.66 | 71.87 | 91.3 |
adapter.ts | 60 | 87.5 | 44.44 | 60 | 41-65
contracts.ts | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 50 | 100 |
mock-adapter.ts | 100 | 90 | 92.3 | 100 | 197
src/__tests__ | 100 | 100 | 100 | 100 |
```
**Coverage Gaps:**
- `adapter.ts` lines 41–65: Unimplemented real adapter methods.
- `mock-adapter.ts` line 197: Edge case in diagnostic output handling (uncovered due to fixed mock data sizes).
- `index.ts` line 50: Factory export (uncovered in isolation tests).
---
## Scope Creep Assessment
**Current scope aligns with PROJECT.md Goal 1 (read-only foundation).**
**Observed scope creep:**
- None detected—adapter factory and config correctly default to `'mock'`.
- `example-config-real.json` suggests real adapter is WIP, not production-ready.
**Not yet in scope (per TODO.md):**
- Diagnostics beyond mock data.
- State-changing actions (reboot, service restart).
- Confirmation flows or dry-run.
---
## TLS and Auth Assumptions (Per Research)
| Requirement | Current Implementation | Research Finding |
|-------------|------------------------|------------------|
| TLS certificate validation | Unknown (no actual connection) | Must be enabled; no `rejectUnauthorized: false` |
| WebSocket endpoint | Not implemented | `wss://host:port/control.ashx` |
| Auth header | Not implemented | `x-meshauth: base64(username,password,2fa?)` |
| Login key support | Not implemented | High sensitivity—avoid unless explicitly chosen |
**Conclusion:** The real adapter is currently a stub that will fail or expose credentials if used with live MeshCentral.
---
## Test Failure Classification
| Test | Failure Type | Fix Required |
|------|--------------|--------------|
| `mock-adapter.test.ts:56` | Expectation mismatch (error message) | Update test expectation |
| `mock-adapter.test.ts:86` | Expectation mismatch (error message) | Update test expectation |
| TypeScript build errors | Type definition conflict | Fix `setup.ts` types |
| ESLint failure | Missing config | Add `.eslintrc.*` |
**Note:** No test failures indicate logic defects in core functionality.
---
## Recommendations
### Immediate (Before Live Integration)
1. **Fix TypeScript errors in `src/__tests__/setup.ts`** to enable build.
2. **Add ESLint configuration** and enforce linting in CI.
3. **Fix test expectations** to align with actual error messages (or standardize messages).
4. **Implement `MeshCentralRealAdapter`** with:
- Secure TLS (enabled validation).
- WebSocket connection to `/control.ashx`.
- `x-meshauth` header authentication.
- Configurable `username`, `password`, or explicit opt-in for login key.
5. **Add audit logging** with structured JSON output.
### Short-Term (Before Production)
6. Implement device/group allowlists.
7. Add output sanitization for diagnostics.
8. Define and enforce diagnostic policies.
9. Add version compatibility checks.
10. Implement retry logic with backoff.
### Medium-Term (After Security Review)
11. Add confirmation flow for reversible actions.
12. Implement dry-run support.
13. Add idempotency and duplicate-execution protection.
---
## Conclusion
The implementation is a **well-structured mock-based scaffold** suitable for local development and testing, but it is not production-ready. The real adapter is **unimplemented** and contains no security controls beyond basic config structure.
**Do not proceed with live MeshCentral integration until:**
- The real adapter is implemented per security requirements.
- All build/lint/test failures are resolved.
- An independent security review confirms TLS, auth, and logging.
---
## Appendix A: File Inventory
| File | Status | Notes |
|------|--------|-------|
| `src/index.ts` | ✅ Clean | Exports adapter factory and contracts |
| `src/adapter.ts` | ⚠️ Stub | Real adapter unimplemented; healthCheck uses wrong endpoint |
| `src/mock-adapter.ts` | ✅ Complete | Full mock with test fixtures |
| `src/contracts.ts` | ✅ Complete | Zod schemas, enums, types |
| `src/__tests__/adapter-factory.test.ts` | ✅ Pass | Factory correctly returns mock by default |
| `src/__tests__/contracts.test.ts` | ✅ Pass | All Zod schema validations pass |
| `src/__tests__/device-status.test.ts` | ✅ Pass | Status enum handling correct |
| `src/__tests__/group-filtering.test.ts` | ✅ Pass | Device filtering by group works |
| `src/__tests__/integration.test.ts` | ✅ Pass | Full workflow mocks succeed |
| `src/__tests__/mock-adapter.test.ts` | ⚠️ 2/6 fail | Expectation mismatch on error messages |
| `src/__tests__/mock-data-loader.test.ts` | ✅ Pass | JSON fixture loading works |
| `src/__tests__/setup.ts` | ❌ Broken | TypeScript errors block build and coverage |
| `src/__tests__/timeout.test.ts` | ✅ Pass | Timeout and output limits respected |
**Test Summary:**
- 50 tests pass (96.2%)
- 2 tests fail due to error message expectation mismatch
- Build fails due to TypeScript type conflicts in test setup
- Linting skipped due to missing ESLint configuration
---
*End of Review Report*