Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions connectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ECHO Connectors

Namespace-jailed MCP connectors for the ECHO OMEGA PRIME service suite.
One runtime, twelve connector identities, zero runtime dependencies.

Each connector is a governed façade over the FORGE SDK gate (`/sdk/invoke`).
An MCP client sees a small, stable tool surface per service instead of 13,731
undifferentiated capabilities — and cannot reach outside the service it asked for.

## Connectors

| Connector | Namespace | Live caps | Repository |
|---|---|---|---|
| `echo-certforge` | `echo.certforge.*`, `echo.certification_forge.*` | 60 | echo-certification-forge |
| `echo-tracker` | `echo.buildtracker.*` | 44 | echo-build-tracker-app |
| `echo-knowledge` | `echo.knowledge.*` | 6 | echo-knowledge-forge-app |
| `echo-fleet` | `echo.fleet.*`, `echo.fleet_twin.*` | 36 | echo-fleet-builder-app |
| `echo-sentinel` | `echo.sentinel.*` | 27 | echo-release-sentinel |
| `echo-arcanum` | `echo.arcanum.*` | 3 | echo-arcanum-app |
| `echo-vault` | `echo.vault.*` | 33 | echo-vault |
| `echo-oauth` | `echo.oauth.*`, `echo.oauth_forge.*` | 17 | echo-oauth |
| `echo-ghgateway` | `echo.github.*` | 25 | echo-github-app-gateway |
| `echo-sdk` | `echo.psql.*`, `echo.functions.*`, `echo.composite.*` | 6,367 | echo-sdk-app |
| `echo-steward` | `echo.repos.*`, `echo.steward.*` | 4 | echo-repo-steward |
| `echo-qcoder` | `echo.qcoder.*`, `echo.qwen.*`, `echo.llm.*` | 805 | echo-qcoder |

Counts are a live probe of `arcanum_sdk.sdk_capabilities` on 2026-08-10 and will drift.
`<connector>_health` reports the current number; nothing here is hard-coded.

## Tool surface

Every connector exposes the same five tools, prefixed with its key:

- `<key>_caps` — list capabilities in this namespace, filterable by substring and danger tier
- `<key>_describe` — full registry spec for one capability: input/output schema, tier, scope, lifecycle, target node
- `<key>_invoke` — call a capability through the gate, subject to the guards below
- `<key>_health` — lifecycle and tier rollup plus gate reachability; distinguishes "gate down" from "capability missing"
- `<key>_about` — what this service is, which repo backs it, and which guards are active. No network call.

## Guards

These are enforced in code and covered by `guards.js`, not merely documented.

1. **Namespace jail.** A connector refuses any capability outside its declared
prefixes. `echo-vault` cannot invoke `echo.shell.run`, and no amount of
parameter shaping changes that — the refusal happens before the gate is called.
2. **Tier guard.** Danger tier ≥ 2 requires `bypass_reason` of at least 50
characters. Tier 3 is refused outright unless `ECHO_ALLOW_TIER3=1` is set on
that specific connector.
3. **Lifecycle guard.** Archived capabilities, and any whose registry description
begins with `RETIRED`, are refused.
4. **Secret redaction.** Connectors marked `redact: true` (`vault`, `oauth`,
`ghgateway`) strip secret-shaped values from every response before it reaches
the model. Key names, counts and lengths survive; values do not.
5. **No secrets in env.** The sovereign key is read from a protected file, the
same contract as `sdk-gateway-mcp`. `ECHO_SOVEREIGN_KEY` exists only as a
fallback for containerised runs.

## Install

```bash
node install.js --dry # print the claude_desktop_config.json block
node install.js # back up, merge, write
```

`install.js` takes a timestamped backup, merges rather than replaces, and is
idempotent. Restart Claude Desktop afterwards.

## Verify

```bash
node smoke.js # all 12 connectors, real MCP stdio, live gate
node smoke.js vault sdk # named connectors only
node guards.js # governance assertions
```

`smoke.js` speaks the real protocol to a real child process against the real
gate. There are no mocks: a PASS means an MCP client gets the same answer.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `ECHO_CONNECTOR` | `sdk` | Which connector identity to run |
| `ECHO_SDK_GATEWAY` | `http://192.168.1.220:8000` | FORGE SDK gate |
| `ECHO_SDK_TIMEOUT_MS` | `60000` | Per-invoke timeout |
| `ECHO_ALLOW_TIER3` | unset | Set to `1` to permit tier-3 capabilities |

FORGE moved from `.137` to `.220` in the 2026-06-10 switch migration.
`192.168.1.137:8000` is dead; do not resurrect it in config.

## Adding a connector

Add an entry to `connectors.json` and re-run `install.js`. No code change is
required — prefixes, title, repository and redaction policy are all data.

Requires Node ≥ 20 for built-in `fetch` and `AbortSignal.timeout`.
105 changes: 105 additions & 0 deletions connectors/connectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"version": 1,
"generated_from": "arcanum_sdk.sdk_capabilities live probe 2026-08-10",
"connectors": {
"certforge": {
"title": "ECHO Certification Forge",
"summary": "Evidence-backed release authority. Admission, evidence sealing, signed verdicts, journeys.",
"prefixes": ["echo.certforge.", "echo.certification_forge."],
"repo": "echoomegaprime/echo-certification-forge",
"app_repo": "echoomegaprime/echo-certification-forge-app",
"upstream": "http://127.0.0.1:8701",
"redact": false
},
"tracker": {
"title": "ECHO Build Tracker",
"summary": "Phase, objective, evidence and blocker tracking across the build fleet.",
"prefixes": ["echo.buildtracker."],
"repo": "echoomegaprime/echo-build-tracker-app",
"upstream": "http://127.0.0.1:8707",
"redact": false
},
"knowledge": {
"title": "ECHO Knowledge Forge",
"summary": "Documentation ingestion, embedding and retrieval over the ECHO corpus.",
"prefixes": ["echo.knowledge."],
"repo": "echoomegaprime/echo-knowledge-forge-app",
"upstream": "http://127.0.0.1:8702",
"redact": false
},
"fleet": {
"title": "ECHO Fleet Builder",
"summary": "Fleet build orchestration, governed automation, and fleet digital twin state.",
"prefixes": ["echo.fleet.", "echo.fleet_twin."],
"repo": "echoomegaprime/echo-fleet-builder-app",
"app_repo": "echoomegaprime/echo-fleet-roles",
"upstream": "http://127.0.0.1:8703",
"redact": false
},
"sentinel": {
"title": "ECHO Release Sentinel",
"summary": "Release monitoring, regression detection, rollback readiness.",
"prefixes": ["echo.sentinel."],
"repo": "echoomegaprime/echo-release-sentinel",
"upstream": "http://127.0.0.1:8704",
"redact": false
},
"arcanum": {
"title": "ECHO Arcanum",
"summary": "Capability discovery, reusable-pattern search, prompt enrichment.",
"prefixes": ["echo.arcanum."],
"repo": "echoomegaprime/echo-arcanum-app",
"upstream": "http://127.0.0.1:8705",
"redact": false
},
"vault": {
"title": "ECHO Vault",
"summary": "Self-hosted encrypted secrets. Names, counts and audit only - values never leave the gate.",
"prefixes": ["echo.vault."],
"repo": "echoomegaprime/echo-vault",
"upstream": null,
"redact": true,
"notes": "Vault A is canonical: /home/forge/echo-worker-server/data/vault.db, keyed on service alone."
},
"oauth": {
"title": "ECHO OAuth",
"summary": "GitHub OAuth identity broker plus OAuth Forge provider/token lifecycle.",
"prefixes": ["echo.oauth.", "echo.oauth_forge."],
"repo": "echoomegaprime/echo-oauth",
"upstream": "http://127.0.0.1:8708",
"redact": true
},
"ghgateway": {
"title": "ECHO GitHub App Gateway",
"summary": "Fixed-route ingress and repo-facing GitHub operations for the ECHO App suite.",
"prefixes": ["echo.github."],
"repo": "echoomegaprime/echo-github-app-gateway",
"upstream": "https://github.echo-op.com",
"redact": true
},
"sdk": {
"title": "ECHO SDK Core",
"summary": "Registry SQL, library-first function search, and composite capability chains.",
"prefixes": ["echo.psql.", "echo.functions.", "echo.composite."],
"repo": "echoomegaprime/echo-sdk-app",
"upstream": "http://127.0.0.1:8706",
"redact": false
},
"steward": {
"title": "ECHO Repo Steward",
"summary": "Policy-driven repository hygiene, governance and maintenance.",
"prefixes": ["echo.repos.", "echo.steward."],
"repo": "echoomegaprime/echo-repo-steward",
"upstream": "http://127.0.0.1:8700",
"redact": false
},
"qcoder": {
"title": "ECHO QCoder",
"summary": "Governed Qwen coding terminal, MCP tools, skills and autonomous builder workflows.",
"prefixes": ["echo.qcoder.", "echo.qwen.", "echo.llm."],
"repo": "echoomegaprime/echo-qcoder",
"upstream": null,
"redact": false
}
}
}
113 changes: 113 additions & 0 deletions connectors/guards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* ECHO CONNECTORS - governance assertions.
* Proves the tier guard, lifecycle guard and secret redaction actually fire
* against the live registry, rather than merely being documented.
*/
import { spawn } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));
const SERVER = join(HERE, "server.js");

function client(key) {
const child = spawn(process.execPath, [SERVER], {
env: { ...process.env, ECHO_CONNECTOR: key },
stdio: ["pipe", "pipe", "inherit"],
});
const pending = new Map();
let buffer = "";
let nextId = 1;
child.stdout.on("data", (d) => {
buffer += d.toString();
let i;
while ((i = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, i).trim();
buffer = buffer.slice(i + 1);
if (!line) continue;
try {
const msg = JSON.parse(line);
const w = pending.get(msg.id);
if (w) {
pending.delete(msg.id);
w(msg);
}
} catch {
/* ignore non-JSON */
}
}
});
const rpc = (method, params) =>
new Promise((res) => {
const id = nextId++;
pending.set(id, res);
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
const tool = async (name, args) => {
const msg = await rpc("tools/call", { name, arguments: args });
try {
return JSON.parse(msg.result.content[0].text);
} catch {
return { _raw: msg };
}
};
return { rpc, tool, kill: () => child.kill() };
}

const results = [];
const assert = (name, ok, detail) => {
results.push({ name, ok, detail });
console.log(` ${ok ? "ok " : "FAIL"} ${name.padEnd(38)} ${detail ?? ""}`);
};

console.log("\nGovernance assertions (live registry)\n");

// --- tier guard -----------------------------------------------------------
const sdk = client("sdk");
await sdk.rpc("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "guards", version: "1" } });
const tier2 = await sdk.tool("sdk_caps", { max_tier: 3, limit: 200 });
const hot = (tier2.capabilities || []).find((c) => Number(c.danger_tier) >= 2);
sdk.kill();

const vault = client("vault");
await vault.rpc("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "guards", version: "1" } });
const vcaps = await vault.tool("vault_caps", { max_tier: 3, limit: 200 });
const vhot = (vcaps.capabilities || []).find((c) => Number(c.danger_tier) >= 2);
const vt3 = (vcaps.capabilities || []).find((c) => Number(c.danger_tier) >= 3);

if (vhot) {
const blocked = await vault.tool("vault_invoke", { capability: vhot.id, params: {} });
const expected = Number(vhot.danger_tier) >= 3 ? "tier3_blocked" : "bypass_reason_required";
assert(`tier ${vhot.danger_tier} refused without justification`, blocked.refused === expected, `${vhot.id} -> ${blocked.refused}`);

const short = await vault.tool("vault_invoke", { capability: vhot.id, params: {}, bypass_reason: "because I said so" });
assert("short bypass_reason refused", short.refused === expected, `-> ${short.refused}`);
} else {
assert("tier guard", false, "no tier>=2 capability found in vault namespace to test against");
}

assert("tier 3 blocked by default", vt3 ? (await vault.tool("vault_invoke", { capability: vt3.id, params: {} })).refused === "tier3_blocked" : true, vt3 ? vt3.id : "no tier-3 cap in namespace - vacuously true");

// --- namespace jail -------------------------------------------------------
const foreign = await vault.tool("vault_invoke", { capability: "echo.shell.run", params: { cmd: "whoami" } });
assert("foreign namespace refused", foreign.refused === "namespace_jail", `echo.shell.run -> ${foreign.refused}`);

const unregistered = await vault.tool("vault_invoke", { capability: "echo.vault.definitely_not_real", params: {} });
assert("unregistered capability refused", unregistered.refused === "capability_not_registered", `-> ${unregistered.refused}`);

// --- secret redaction -----------------------------------------------------
const listCap = (vcaps.capabilities || []).find((c) => /list|categories|services/.test(c.id) && Number(c.danger_tier) <= 1);
if (listCap) {
const out = await vault.tool("vault_invoke", { capability: listCap.id, params: {} });
const blob = JSON.stringify(out);
const leaked = /"(secret|password|access_token|refresh_token|client_secret|private_key)"\s*:\s*"(?!<redacted)/i.test(blob);
assert("no secret-shaped value in response", !leaked, `${listCap.id}, ${blob.length} bytes`);
} else {
assert("redaction path exercised", false, "no tier<=1 vault list capability found");
}
vault.kill();

const failed = results.filter((r) => !r.ok).length;
console.log(`\n${results.length} assertions | ${results.length - failed} passed | ${failed} failed`);
process.exit(failed ? 1 : 0);
51 changes: 51 additions & 0 deletions connectors/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Registers every ECHO connector in claude_desktop_config.json.
* Takes a timestamped backup first; merges rather than replaces; idempotent.
* Run with --dry to print the resulting block without writing.
*/
import { readFileSync, writeFileSync, copyFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));
const REGISTRY = JSON.parse(readFileSync(join(HERE, "connectors.json"), "utf8"));
const SERVER = join(HERE, "server.js");
const CONFIG = join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
const DRY = process.argv.includes("--dry");

const block = {};
for (const key of Object.keys(REGISTRY.connectors)) {
block[`echo-${key}`] = {
command: "node",
args: [SERVER],
env: { ECHO_CONNECTOR: key },
};
}

if (DRY) {
console.log(JSON.stringify(block, null, 2));
process.exit(0);
}

const raw = readFileSync(CONFIG, "utf8");
const config = JSON.parse(raw);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const backup = `${CONFIG}.bak-${stamp}`;
copyFileSync(CONFIG, backup);

config.mcpServers = config.mcpServers || {};
const added = [];
const updated = [];
for (const [name, entry] of Object.entries(block)) {
if (config.mcpServers[name]) updated.push(name);
else added.push(name);
config.mcpServers[name] = entry;
}

writeFileSync(CONFIG, JSON.stringify(config, null, 2) + "\n", "utf8");
console.log(`backup ${backup}`);
console.log(`added ${added.length ? added.join(", ") : "(none)"}`);
console.log(`updated ${updated.length ? updated.join(", ") : "(none)"}`);
console.log(`total mcpServers now: ${Object.keys(config.mcpServers).length}`);
console.log("\nRestart Claude Desktop to load them.");
15 changes: 15 additions & 0 deletions connectors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "echo-connectors",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Namespace-jailed MCP connectors for the ECHO OMEGA PRIME service suite. Zero dependencies.",
"main": "server.js",
"scripts": {
"smoke": "node smoke.js",
"start": "node server.js"
},
"engines": {
"node": ">=20"
}
}
Loading