Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@
# Flipping WAVEX_COMPOSIO_DISABLED=0 switches the connectors directory
# from the curated 16-toolkit fallback to Composio's live catalog
# (~200+ toolkits with logos + categories).
# NOTE: WAVEX_COMPOSIO_DISABLED=0 (live) is INERT until COMPOSIO_API_KEY is
# filled — with a blank key, live mode returns a "key missing" error and the
# directory shows its empty state. The code's dev default (this var unset and
# NODE_ENV!=production) is disabled / curated-fallback.
COMPOSIO_API_KEY=
WAVEX_COMPOSIO_DISABLED=0
# WaveX-managed mode: operator runs WaveX-as-a-service and provides the
# Composio key server-side; end users never see the BYO-key form and are
# billed via their subscription tier instead. POST /api/connectors/enable-managed
# sets this at runtime; set it here to start in managed mode.
# WAVEX_COMPOSIO_MANAGED=0
# Origin the onboarding UI's Vite dev proxy forwards /api + /wavex-os to
# (the mock-core sidecar). Defaults to http://127.0.0.1:3101.
# WAVEX_CORE_URL=http://127.0.0.1:3101

# ── Anthropic (T2 tier calls in apikey mode) ────────────────────────
# Only needed when WAVEX_INFERENCE_MODE=apikey (default in production).
Expand Down
4 changes: 2 additions & 2 deletions packages/paperclip-plugin-wavex/src/ui/ConnectorsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* filter/sort, grouped cards. Each card has a brand logo + name +
* "#N popular" badge + add (+) button.
* - Add triggers real OAuth via the existing
* POST /op-omega/onboarding/connectors/oauth/initiate route,
* POST /wavex-os/onboarding/connectors/oauth/initiate route,
* opening the redirect URL in a new tab and polling the vault
* until the slug shows vaulted_valid (then the card flips to
* connected).
* - Disconnect calls DELETE /api/connectors/:companyId/:slug.
*
* Backend reuse: connect/list/callback exist in op-omega-server's
* Backend reuse: connect/list/callback exist in wavex-os-server's
* connectors.ts; connected-status + disconnect routes I added to
* credentials.ts. No new tables.
*/
Expand Down
8 changes: 4 additions & 4 deletions packages/paperclip-plugin-wavex/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Registers data + action handlers consumed by the Mission Control UI
* widgets and the Inception Status sidebar. The only outbound HTTP
* target is the wavex-os op-omega-server (default http://127.0.0.1:3101).
* target is the wavex-os-server (default http://127.0.0.1:3101).
*
* No writes through this plugin. State-changing actions still flow
* through Paperclip's native commands or the wavex MC routes — the
Expand All @@ -27,7 +27,7 @@ const PAPERCLIP_BASE = "http://127.0.0.1:3100";
*
* The host's plugin-bridge HTTP client refuses any URL that resolves to a
* private/loopback IP as an SSRF guard. Mission Control's data source is
* the wavex op-omega-server sibling on 127.0.0.1:3101 — strictly local —
* the wavex-os-server sibling on 127.0.0.1:3101 — strictly local —
* so we use the worker process's native fetch, which is not subject to
* that guard. This keeps the wavex plugin honest about its only outbound
* target (the same machine the host is on) without weakening the
Expand Down Expand Up @@ -1286,8 +1286,8 @@ const plugin = definePlugin({
);

// -------------------------------------------------------------------
// inception-status — reads /api/companies/<id>/agents from op-omega
// server. Returns ready/total counts + manifest goal/signed_at.
// inception-status — reads /api/companies/<id>/agents from the
// wavex-os-server. Returns ready/total counts + manifest goal/signed_at.
// -------------------------------------------------------------------
ctx.data.register("inception-status", async ({ companyId }) => {
const cfg = (await ctx.config.get()) as PluginConfig | null;
Expand Down
27 changes: 23 additions & 4 deletions packages/wavex-os-server/src/routes/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ async function writeOrUpdateEnvFile(
updates: Record<string, string>,
): Promise<void> {
const { readFile, writeFile } = await import("node:fs/promises");
// Defense-in-depth: never let a value carry a newline/CR that would split
// into an extra `.env` line and inject an unrelated env var.
for (const [k, v] of Object.entries(updates)) {
if (/[\r\n]/.test(v)) {
throw new Error(`env value for ${k} contains illegal newline characters`);
}
}
let existing = "";
try {
existing = await readFile(envPath, "utf8");
Expand All @@ -256,7 +263,9 @@ async function writeOrUpdateEnvFile(
}
// Trim trailing blank lines, ensure single newline at end.
while (next.length > 0 && next[next.length - 1]!.trim() === "") next.pop();
await writeFile(envPath, `${next.join("\n")}\n`, "utf8");
// Restrictive perms: the .env may hold a plaintext Composio key; avoid a
// world-readable (0644) file.
await writeFile(envPath, `${next.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
}

export function registerCredentialRoutes(app: FastifyInstance): void {
Expand Down Expand Up @@ -391,7 +400,8 @@ export function registerCredentialRoutes(app: FastifyInstance): void {
// to <repo>/.env, mutates process.env in-memory, and resets the
// composio-shim cached client. No process restart required.

app.get("/api/connectors/setup-status", async (_req, reply) => {
app.get("/api/connectors/setup-status", async (req, reply) => {
if (!gateBoard(req, reply)) return;
const hasKey = Boolean((process.env.COMPOSIO_API_KEY ?? "").trim());
const disabled = (process.env.WAVEX_COMPOSIO_DISABLED ?? "").toLowerCase();
const isDisabled = disabled === "1" || disabled === "true" || disabled === "yes";
Expand Down Expand Up @@ -449,10 +459,17 @@ export function registerCredentialRoutes(app: FastifyInstance): void {
app.post<{ Body: { apiKey?: string } }>(
"/api/connectors/setup",
async (req, reply) => {
if (!gateBoard(req, reply)) return;
const apiKey = (req.body?.apiKey ?? "").trim();
if (!apiKey) {
return reply.code(400).send({ ok: false, error: "apiKey is required." });
}
// Reject control characters: the value is later written verbatim into
// the repo .env as `KEY=value`; an embedded newline would inject
// arbitrary additional env lines (e.g. ANTHROPIC_API_KEY=...).
if (/[\r\n]/.test(apiKey)) {
return reply.code(400).send({ ok: false, error: "apiKey contains illegal characters." });
}
// 1. Mutate process.env so the next getClient() call picks up
// the new key (composio-shim's getComposioApiKey() re-reads
// process.env on every call, and we reset the cached client
Expand Down Expand Up @@ -519,7 +536,8 @@ export function registerCredentialRoutes(app: FastifyInstance): void {
// key-entry screen. If no server-side master key is configured, the
// catalog renders its empty state ("activates with your subscription")
// rather than the BYO-key form — same final UX, no secret in the browser.
app.post("/api/connectors/enable-managed", async (_req, reply) => {
app.post("/api/connectors/enable-managed", async (req, reply) => {
if (!gateBoard(req, reply)) return;
process.env.WAVEX_COMPOSIO_MANAGED = "1";
process.env.WAVEX_COMPOSIO_DISABLED = "0";
resetComposioClient();
Expand All @@ -546,7 +564,8 @@ export function registerCredentialRoutes(app: FastifyInstance): void {
* built from this. Cached in-process for 5 min — the catalog is
* ~200+ entries and rarely changes. */
let _cachedCatalog: { ts: number; rows: Array<{ slug: string; name: string; logo?: string; description?: string; category?: string; noAuth?: boolean; authSchemes?: string[] }>; source: "composio" | "curated" } | null = null;
app.get("/api/connectors/catalog", async (_req, reply) => {
app.get("/api/connectors/catalog", async (req, reply) => {
if (!gateBoard(req, reply)) return;
const now = Date.now();
if (_cachedCatalog && now - _cachedCatalog.ts < 5 * 60_000) {
return reply.send({ ok: true, ...{ rows: _cachedCatalog.rows, source: _cachedCatalog.source, cached: true } });
Expand Down
Loading