From 5fd421a10d1a4e8d849c96ad386c3474a72d3a3c Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sat, 18 Jul 2026 20:12:29 +0400 Subject: [PATCH 1/2] docs: make README product-first --- README.md | 219 ++++++++------------------ tests/readme-product-contract.test.ts | 32 ++++ 2 files changed, 101 insertions(+), 150 deletions(-) create mode 100644 tests/readme-product-contract.test.ts diff --git a/README.md b/README.md index 4009b5ab..3a614a63 100644 --- a/README.md +++ b/README.md @@ -1,206 +1,125 @@ # Miftah -**Wrap any MCP. Use the right account without reconnecting.** +## One MCP connector. Deliberate account selection. -Miftah (`@lubab/miftah`) is a local MCP auth wrapper and credential broker for multi-account workflows. It sits between an MCP client such as Claude Desktop and an existing upstream MCP server, injects the selected profile's credentials, forwards MCP operations, and records redacted audit metadata. +You already have an MCP for GitHub, Sentry, PostHog, or another service. The hard part starts when you use that same MCP service across more than one account, client, project, or environment. -Miftah is **not** a replacement for GitHub MCP, Sentry MCP, PostHog MCP, or any other provider MCP. It wraps them. The upstream server remains responsible for provider behavior; Miftah handles profile selection, secret injection, lifecycle, routing, policy, and redaction. +Miftah is a local MCP profile manager and safety layer. It wraps the upstream server you already use, gives it named profiles such as `personal`, `work`, `client-a`, or `production`, and keeps account selection intentional. -> **Status:** Miftah is experimental and pre-1.0. Interfaces and security behavior may change between minor versions. See the [release policy](CHANGELOG.md#release-policy) and use the [private disclosure process](SECURITY.md) for vulnerabilities. - -## Install - -For a published release: - -```bash -npm install -g @lubab/miftah -``` - -Miftah runs locally by default. It has no cloud dependency or telemetry. - -## Quick start - -Generate a safe template: - -```bash -miftah init github --preset github --output ~/.config/miftah/github.json --client claude-desktop -``` +Miftah runs locally by default. Miftah itself has no cloud service or telemetry; it connects only to the upstreams you configure. -Set the generated profile environment references in the environment that launches your client, then validate the configuration: - -```bash -miftah validate --config ~/.config/miftah/github.json -``` - -`init --client` prints a host-specific JSON snippet with absolute launcher paths. Copy that JSON to the appropriate client configuration; Miftah does not write a client file. See the [preset and client compatibility matrix](docs/presets-and-clients.md) for the catalog pin, client location, and security boundaries. +> **Status:** Miftah is experimental and pre-1.0. Interfaces and security behavior may change between minor versions. See the [release policy](CHANGELOG.md#release-policy) and use the [private disclosure process](SECURITY.md) for vulnerabilities. -Run a wrapped server directly when testing local STDIO: +## Why Miftah exists -```bash -miftah --config ~/.config/miftah/github.json -``` +Without Miftah, a multi-account setup usually becomes a growing list of client entries: `github-personal`, `github-work`, `sentry-client-a`, `sentry-client-b`, and so on. That works at first, but it makes the connection configuration—not the account context—the thing you have to manage. -To serve a local Streamable HTTP endpoint instead, configure `server.http` and run: - -```bash -miftah serve --transport http --config ~/.config/miftah/github.json -``` +| Instead of this | Miftah gives you this | +| --- | --- | +| One client entry for every account | One Miftah connector per service, with named profiles for each account or environment | +| Repeated launch settings and credential wiring | Profile-scoped credential references that stay outside client configuration | +| Manual connector selection with little context | Explicit profile switching and optional routing rules for stable, known context | +| No consistent local record of what happened | Optional, redacted local audit metadata and health/diagnostic tools | -The default endpoint is `http://127.0.0.1:3000/mcp`; see [HTTP server transport](docs/config.md#http-server-transport) before exposing any non-loopback address. +Do not create one client entry for every account. Add one Miftah connector per service, then manage the accounts behind it as profiles. -## Profiles +## What Miftah does today -Profiles are named credential environments. Keep secret values outside JSON and use the exact generated references in the checked-in [GitHub](examples/github.miftah.json), [Sentry](examples/sentry.miftah.json), or [generic reference](examples/generic.miftah.json) example. The strict catalog pins GitHub to `ghcr.io/github/github-mcp-server:v1.5.0` with its documented read-only tool configuration; it does not claim a digest. The [compatibility matrix](docs/presets-and-clients.md) describes safe promotion and deployment recording for that tag. +### Keeps account context explicit -Claude can call `miftah_list_profiles`, `miftah_current_profile`, `miftah_use_profile`, `miftah_reset_profile`, `miftah_lock_profile`, `miftah_unlock_profile`, `miftah_profile_info`, `miftah_health`, `miftah_validate_config`, `miftah_list_upstream_tools`, `miftah_restart_profile`, `miftah_verify_identity`, `miftah_route_preview`, and `miftah_list_approvals`. `miftah_approve` and `miftah_deny` are intentionally advertised only when an operator explicitly sets `security.approvalMode` to `"delegated-agent"`; their names remain reserved in every mode. In a multi-upstream bundle, upstream tools are exposed as `__`. For a single upstream whose exact tool name collides with a reserved management name, the default is `upstream_`; `tooling.collisionStrategy: "fail"` instead rejects it. Other upstream names that merely start with `miftah_` are not reserved. After a profile change, restart, upstream failure, recovery, or upstream list-change notification that changes a public capability surface, MCP clients receive list-change notifications and should re-list the affected tools, resources, resource templates, or prompts before relying on cached capabilities. +Profiles represent real working contexts—not just different tokens. You can switch deliberately, keep a safe configured default, or add routing rules for stable signals such as a repository, organization, or project. If routing is ambiguous, Miftah does not guess. -Active profile state is in-memory by default. `state.scope: "session"` resets on a new MCP transport; opt-in `workspace` or config-identity-namespaced `global` scope persists only safe selection metadata (the profile and timestamp) using atomic owner-restricted storage. Clients cannot choose a scope or state path. Optional runtime locks and risk leases are connection-bound and never enter that durable state. See [active profile state](docs/config.md#active-profile-state) for lock precedence, fallback diagnostics, and platform paths. +### Adds a control layer around existing MCPs -For credential-file workflows, opt in to [profile credential isolation](docs/config.md#profile-credential-isolation). It gives each POSIX profile/upstream target a Miftah-managed HOME/XDG tree, can copy approved configuration-relative files into it, and can generate fixed Docker/Podman bind-mount arguments. It is deliberately not a sandbox for a hostile native process running as the same OS user. +Miftah wraps an existing upstream MCP server. It does not replace it. Your GitHub, Sentry, PostHog, or other provider MCP continues to provide its tools and provider behavior; Miftah handles the local profile, credential, routing, policy, approval, lifecycle, and redaction boundaries around it. -For account bundles, define `upstreams` instead of `upstream`. Tools, resource names, resource-template names, and prompt names are exposed as `__` (for example `github__search_issues` and `github__account_prompt`), and each profile can provide per-upstream environment or header overrides. A multi-upstream resource URI is a Miftah-owned route such as `miftah://resource/github?uri=account%3A%2F%2Fcurrent`; resource templates use opaque Miftah-owned template bases. Their encoded values are redacted before exposure, while Miftah retains the exact original upstream URI or URI template only in its per-profile route map. Before exposing multi-upstream resource or prompt URI metadata, Miftah strips URI userinfo and fragments and redacts every query value. Reads and prompt gets are routed only through the exact route map, never by guessing from a client identifier. +### Keeps secrets out of client configuration -For repeatable account selection, opt in to [provider routing matchers](docs/config.md#provider-routing-matchers) on a profile. Miftah's fixed built-in GitHub, Sentry, Jira, Linear, and PostHog matchers use only typed canonical identifiers and safe context, including an allowlisted canonical HTTPS provider URI on a standard resource read. Explicit hints and routing rules remain stronger, a matcher never authorizes a destructive operation by itself, and competing profile matches fail closed. For an explicit local extension, use the versioned [plugin API](docs/plugins.md); plugins run in contained child hosts and remain operator-trusted local code. +Profiles refer to credentials from environment variables, dotenv files, OS keychains, 1Password, or an explicitly trusted local provider. Secret values do not belong in your MCP client JSON, and Miftah redacts them from its diagnostics and audit output. -Multi-upstream resource, resource-template, and prompt lists use opaque, bounded in-memory Miftah cursors so individual upstream cursors are not exposed. Re-list after a profile change, restart, or upstream list-change notification instead of retaining an old cursor. When every selectable profile and upstream supports subscriptions, Miftah advertises `resources.subscribe`, proxies subscribe/unsubscribe calls through the same resource-read routing and policy boundary, and forwards only matching Miftah-namespaced resource-update URIs. Miftah probes those finite upstream capability handshakes sequentially, closes each probe session before accepting the client connection, and therefore does not guess an immutable MCP capability or hold profile capacity for the probe. Subscription transitions for one URI are serialized; a profile change clears every connection-bound subscription and bounds upstream cleanup by `process.shutdownTimeoutMs`. A standard `upstream` or a one-entry `upstreams` map preserves credential-free raw resource URIs, prompt names, and native upstream cursors for compatibility; URI and icon fields with userinfo, query values, or fragments are structurally redacted before they cross the boundary. A zero-entry bundle still omits resource and prompt capabilities. +### Makes sensitive work more deliberate -`tooling.toolDiscoveryMode` defaults to `"permissive"`. In that mode, a bundled list returns only healthy upstream contributions, removes routes for failed upstreams, and retries incomplete tool discovery on later tool requests. If every upstream fails a capability list, Miftah returns `UPSTREAM_DISCOVERY_FAILED` instead of advertising an empty upstream surface. Set it to `"strict"` to reject any unavailable upstream; strict tool discovery additionally checks every configured profile for an identical client-visible tool contract. `miftah_health` reports the profile, upstream name, overall and process state, transition time, automatic restart count, redacted error, intentional stop reason, pending recovery time, and per-capability discovery state for each started upstream. See `examples/multi-upstream.miftah.json`. +Optional policies, explicit destructive-profile selection, confirmation flows, identity checks, and local audit records help keep a helpful agent from using a plausible-but-wrong account or silently proceeding with sensitive work. -## Remote upstreams +## Get running with Claude Desktop -Use `"streamable-http"` for a remote MCP server: +Install Miftah: -```json -{ - "upstream": { - "transport": "streamable-http", - "url": "https://mcp.example.com/mcp", - "headers": { - "Authorization": "Bearer ${MCP_TOKEN}" - } - }, - "profiles": { - "work": { - "headers": { - "authorization": "Bearer ${WORK_MCP_TOKEN}" - } - } - } -} +```bash +npm install -g @lubab/miftah ``` -Header names are case-insensitive, and a profile header always replaces the upstream header of the same name. Miftah requires HTTPS for non-loopback remote URLs; HTTP is accepted only for local development endpoints on `localhost`, `127.0.0.0/8`, or `::1`. The `"http"` transport is a version-1-only compatibility alias for `"streamable-http"`; version 2 requires `"streamable-http"`. `"sse"` remains available for legacy MCP servers but is deprecated; prefer Streamable HTTP for new deployments. +Generate a GitHub configuration and a Claude Desktop snippet: -Remote authentication currently means explicitly configured static headers; Miftah does not yet run an upstream OAuth browser, callback, refresh, or revoke flow. Read [OAuth support](docs/oauth-support.md) before assuming a provider's remote or local OAuth mechanism is compatible. - -An intentional Streamable HTTP restart or wrapper shutdown sends the MCP session DELETE request before closing the local client transport. A server may decline DELETE with HTTP 405, in which case Miftah still closes its local session but the remote server controls any remaining server-side state. If DELETE does not settle before the configured shutdown deadline, Miftah aborts the local transport rather than leaving a live credential session behind. Requested cancellation and requested progress notifications are forwarded across both local STDIO and Streamable HTTP upstreams. HTTP status failures are returned as `UPSTREAM_HTTP_ERROR` and MCP JSON-RPC failures as `UPSTREAM_PROTOCOL_ERROR`; response bodies and remote error messages are not exposed to callers. +```bash +miftah init github --preset github --output ~/.config/miftah/github.json --client claude-desktop +``` -Miftah uses the SDK's existing 60-second MCP request timeout and does not add a second request-timeout setting. Startup and shutdown retain the configured 30-second and 5-second defaults. The SDK performs its bounded Streamable HTTP SSE reconnection behavior; if the transport ultimately closes, Miftah's opt-in `process.restartOnCrash` policy and bounded `maxRestarts` budget control profile recovery. +The GitHub preset requires Docker and generates `GITHUB_WORK_TOKEN` and `GITHUB_PERSONAL_TOKEN` credential references. Set those references in the environment that launches Claude Desktop, then validate the configuration: -## Routing and safety +```bash +miftah validate --config ~/.config/miftah/github.json +``` -Routing can use the active profile or rules matching tool arguments: +`miftah validate` checks the configuration; it does not start the upstream server or prove that a credential works. For a redacted readiness check after you have set the references, run: -```json -{ - "routing": { - "mode": "hybrid", - "fallback": "ask", - "rules": [ - { - "name": "work-repository", - "when": { "args.repo": "my-org/work" }, - "profile": "work" - } - ] - } -} +```bash +miftah doctor --config ~/.config/miftah/github.json ``` -When several profiles match, Miftah refuses to guess. Use explicit profile switching for write and destructive actions. The same routing, policy, redaction, and audit pipeline applies to upstream tool calls, resource reads and subscriptions, and prompt retrieval. Policy patterns use each upstream tool's original name for tools, `resources/read` for reads and subscriptions, and `prompts/get` for prompt retrieval. A deny, blocked, or ambiguous decision is returned before Miftah forwards the request. A confirmation-required operation pauses for a connection-bound, one-time approval. The default `security.approvalMode: "human"` uses a generic form only with clients that support MCP form elicitation; a client without that capability fails closed and receives no bearer. An operator may explicitly choose `"delegated-agent"` for automation, which exposes a short-lived bearer and `miftah_approve`/`miftah_deny`; that is delegated agent authorization, never proof of a human decision. Provider token scopes still matter: local policy cannot make a write-capable provider token read-only. Profiles that set a policy name must reference an existing entry in `policies`, while profiles with no `policy` field keep the default allow behavior. +Claude Desktop is a GUI app and does not inherit terminal startup files such as `~/.zshrc`; use a supported secret provider or a GUI-visible launcher environment instead of relying on shell exports alone. -Miftah can also match bounded workspace metadata without treating a project file as configuration. It resolves a valid environment hint, then the nearest valid project-marker hint, then matching rules over tool arguments and collected context, then the configured fallback. Rules that select different profiles return `ROUTING_AMBIGUOUS`, and a context hint never authorizes a destructive operation that requires an explicit rule. `miftah_route_preview` and eligible audit records expose only sanitized routing evidence plus risk-classification source/confidence, not raw project environment values, upstream metadata, or project file contents. See [routing context](docs/config.md#routing-context) for the marker schema, root behavior, and evidence boundary. +`init --client` prints JSON with absolute launcher paths for you to copy into your client configuration. It deliberately does not modify the client file. -## Identity verification +Continue with the step-by-step [Claude Desktop setup](docs/claude-desktop.md), or start from the [GitHub example](docs/examples/github.md) or [Sentry example](docs/examples/sentry.md). -Identity verification is an optional account-fingerprint check, not provider authentication or authorization. Configure it under `profiles..identity`, or under `profiles..upstreams..identity` to replace that profile-level configuration for one named upstream. It can require a fresh matching fingerprint before explicitly configured write or destructive operations execute. Read discovery, resources, and prompts are not identity-gated. +## A real multi-account setup -`miftah_current_profile`, `miftah_health`, and `miftah_route_preview` show configured or cached safe status without starting an upstream or probing it. Use `miftah_verify_identity` to explicitly refresh configured targets; it returns safe structured status even if verification does not complete. See [identity verification configuration](docs/config.md#identity-verification) and the [security boundary](docs/security.md#identity-verification-boundary). +Imagine you use Sentry for two products. Instead of adding two separate Sentry servers to Claude Desktop, configure one `miftah-sentry` connector with two profiles: -## Secret handling +```text +miftah-sentry +├── product-a +└── product-b +``` -Supported local references include environment variables (`${NAME}` and `secretref:env://NAME`), configured dotenv files (`secretref:dotenv://NAME`), explicitly opt-in plaintext (`secretref:plain://...`), OS keychain entries (`secretref:keychain:///`), 1Password fields (`secretref:op:////`), and explicitly allowlisted local plugin schemes (`secretref:://...`). External reference components are strictly percent-decoded once; keep credentials out of references and config files. Secrets are redacted from diagnostics, errors, stderr forwarding, audit entries, and tool responses. See [secret provider configuration](docs/config.md#secret-providers) and the [local plugin API](docs/plugins.md) for bounded execution and trust boundaries. +When you need the other product, select its profile deliberately. When a tool call needs it, Miftah starts or reuses the corresponding upstream context with that profile's credentials, while the client keeps one Sentry connector. The same pattern works for personal/work GitHub accounts, client environments, and staging/production services. -Use `miftah doctor` to inspect config and upstream readiness without printing process environment values. +## What it works with -## Audit logging +- **GitHub and Sentry:** strict built-in presets and generated client snippets. +- **Other compatible MCP servers:** configure a generic STDIO, Streamable HTTP, or legacy SSE upstream, then add profiles around it. +- **Multiple upstreams in one wrapper:** use an account bundle when related upstreams belong behind one controlled connection. -Set `audit.path` to record one terminal JSONL event for every supported MCP operation, including discovery, management, tool, resource, and prompt requests. Events include a per-process session ID, request/event ID, source and selected profiles, upstream, routing and policy metadata where applicable, terminal outcome, stable error code, and duration. Route previews and proxied operations additionally include sanitized `routingEvidence` from their collector snapshot. Wrapper and upstream lifecycle transitions are recorded separately. Arguments are omitted unless `audit.includeArguments` is `true`. +Miftah does not run provider OAuth browser, callback, refresh, or revocation flows. If an upstream owns its OAuth flow, complete that flow with the upstream or provider and then configure Miftah around the resulting supported connection. Read [OAuth support](docs/oauth-support.md) before assuming a provider OAuth model is compatible. -New audit directories and files use owner-only permissions where the platform supports them. `audit.failureMode` defaults to `"fail-closed"`, which verifies the audit sink before dispatch and refuses the request if it cannot be prepared. A terminal write can still fail after an upstream side effect completes, so treat a post-dispatch `AUDIT_WRITE_FAILED` as an indeterminate outcome and do not blindly retry non-idempotent tools. Set it to `"fail-open"` only when availability outweighs that guarantee; the original operation remains available and `miftah_health` reports a redacted `AUDIT_WRITE_FAILED` audit-health entry. +## Trust and control boundaries -Optional rotation keeps a bounded local journal without breaking JSONL records: +Miftah supports environment and dotenv references, plus OS keychain references such as `secretref:keychain:///` and 1Password references such as `secretref:op:////`. The reference is configuration; the secret value stays outside the MCP client configuration. -```json -{ - "audit": { - "path": "./audit/events.jsonl", - "rotation": { - "maxBytes": 10485760, - "maxAgeMs": 86400000, - "retainFiles": 14 - }, - "integrity": { "algorithm": "sha256-chain" } - } -} -``` +For credential-file workflows, see [profile credential isolation](docs/config.md#profile-credential-isolation). Where provider/account signals are stable, opt-in [provider routing matchers](docs/config.md#provider-routing-matchers) and [routing context](docs/config.md#routing-context) can inform profile selection; ambiguous context never selects an account by guesswork. -At least one rotation trigger is required; `retainFiles` is the number of completed archive segments to keep in addition to the active file (maximum `2000`). Rotation and integrity require a non-empty `audit.path` and cannot be combined with `audit.enabled: false`. Miftah manages only its own regular archive files in the configured audit directory and refuses unsafe paths. Journal coordination is local to one host, so do not concurrently share a managed journal through a network filesystem. The optional integrity chain provides local tamper evidence across retained segments; use `miftah audit-verify --config ` to report the first safe broken location. It is not a signature or a remote immutable log, so preserve required evidence in an independently protected system. +When an operator enables profile locking, `miftah_lock_profile` and `miftah_unlock_profile` expose that control to the MCP client. For the complete security scope and future work, read the linked designs below. -`miftah audit-export --config --output ` is an explicit local support export: it never uploads telemetry, redacts records again, and removes stored `arguments` by default. `--include-arguments` is an intentional opt-in and still redacts those values. +## Is Miftah right for you? -## CLI +Miftah is a good fit when account context matters: you work across clients, organizations, environments, or credentials; you want a deliberate boundary before write/destructive actions; or you want one local place to inspect safe, redacted connection activity. -Use `miftah --help` for the generated command list and `miftah --help` for command-specific options. The available commands are: +If you use one account with one direct MCP server and do not need profile, routing, policy, or audit controls, a direct MCP entry is simpler. Miftah is not an MCP marketplace and does not add provider functionality that the upstream server does not have. -| Command | Purpose | -| --- | --- | -| `miftah --config ` / `miftah serve --config ` | Run the STDIO MCP wrapper. Add `--transport http` to serve the configured local Streamable HTTP endpoint. | -| `miftah validate --config ` | Parse and validate JSON config; writes JSON. | -| `miftah doctor --config [--json]` | Report redacted configuration and upstream readiness. | -| `miftah init [name] [--name ] [--preset ] [--output ] [--interactive] [--client ] [--credential-env ] [--npm-package ] [--docker-image ] [--url ] [--header-name ] [--header-prefix ]` | Generate a strict catalog template and optionally print client JSON. | -| `miftah migrate-config --config [--write]` | Plan an explicit configuration-format migration; `--write` creates an exact backup before a same-directory non-overwriting publication. | -| `miftah schema` | Print the JSON Schema. | -| `miftah list-tools --config [--profile ]` | Discover upstream tools as JSON. | -| `miftah test-profile --config [--profile ]` | Start and initialize one profile; writes JSON. | -| `miftah logs --config [--follow]` | Read normalized, redacted audit JSONL; follow rotation safely when requested. | -| `miftah audit-export --config --output [--include-arguments]` | Create an explicit, redacted audit support export. | -| `miftah audit-verify --config [--json]` | Verify configured local audit-chain integrity without starting an upstream. | -| `miftah --version` / `miftah -v` / `miftah version [--json]` | Print the package SemVer. `--json` intentionally preserves bare SemVer output. | - -Structured success output is written to stdout with stderr empty. Stable nonzero categories are usage (`2`), configuration (`3`), secret resolution (`4`), upstream (`5`), and policy (`6`); `1` is an uncategorized operational failure. Quote config and output paths with spaces. `logs --follow` handles appends, truncation, and rotation, and exits cleanly on `SIGINT` or `SIGTERM` without starting an upstream. See the complete [CLI reference](docs/cli.md) for help behavior, defaults, JSON contracts, redaction, and audit reader boundaries. - -## Documentation +## Learn more -- [Architecture](docs/architecture.md) -- [Configuration](docs/config.md) -- [Security](docs/security.md) -- [Threat model](docs/threat-model.md) -- [OAuth and Console security design](docs/oauth-console-threat-model.md) -- [Security reporting](SECURITY.md) -- [CLI](docs/cli.md) -- [Library API](docs/library-api.md) -- [Claude Desktop](docs/claude-desktop.md) +- [Claude Desktop setup](docs/claude-desktop.md) - [Preset and client compatibility](docs/presets-and-clients.md) -- [GitHub example](docs/examples/github.md) -- [Sentry example](docs/examples/sentry.md) +- [GitHub example](docs/examples/github.md) and [Sentry example](docs/examples/sentry.md) +- [Configuration reference](docs/config.md) +- [Security boundary](docs/security.md), [Threat model](docs/threat-model.md), and [OAuth and Console security design](docs/oauth-console-threat-model.md) +- [OAuth support](docs/oauth-support.md) +- [CLI reference](docs/cli.md) +- [Architecture](docs/architecture.md) - [Changelog and release policy](CHANGELOG.md) - [Contributing](CONTRIBUTING.md) -## Current boundaries - -The current experimental code implements local STDIO plus loopback-first Streamable HTTP serving, remote HTTP/SSE upstream clients, profile switching with opt-in scoped persistence, hybrid routing rules plus fixed and explicitly allowlisted local plugin matchers, typed secret-provider plugins, policies, optional upstream identity verification, namespaced tools/resources/prompts for account bundles, resilient healthy-upstream discovery, configurable local process lifecycle controls, in-memory process/session caching, rotated redacted JSONL audit journals with optional local tamper evidence, and a packageable CLI. HTTP sessions are bounded, isolated, and cleaned up on expiry, DELETE, or shutdown; local process controls cover startup and shutdown deadlines, optional idle cleanup, opt-in crash recovery with a bounded retry budget, and no-eviction profile-session capacity limits. UI, legacy `routing.plugins`, `process.startMode`, `process.cache`, custom state paths, and configurable tool namespaces are rejected with `UNSUPPORTED_CONFIG_OPTION` rather than silently ignored. - ## License MIT diff --git a/tests/readme-product-contract.test.ts b/tests/readme-product-contract.test.ts new file mode 100644 index 00000000..0a79e696 --- /dev/null +++ b/tests/readme-product-contract.test.ts @@ -0,0 +1,32 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); + +describe("product README", () => { + it("leads with the multi-account outcome instead of internal implementation terms", () => { + expect(readme).toContain("## One MCP connector. Deliberate account selection."); + expect(readme).toContain("same MCP service across more than one account"); + expect(readme).toContain("Do not create one client entry for every account."); + expect(readme).not.toContain("credential broker"); + }); + + it("sets practical expectations for local operation, audit logging, and GUI secret setup", () => { + expect(readme).toContain("Miftah itself has no cloud service or telemetry"); + expect(readme).toContain("Optional, redacted local audit metadata"); + expect(readme).toContain("Claude Desktop is a GUI app and does not inherit terminal startup files"); + }); + + it("explains what Miftah changes and what it deliberately does not replace", () => { + expect(readme).toContain("one Miftah connector per service"); + expect(readme).toContain("Miftah wraps an existing upstream MCP server. It does not replace it."); + expect(readme).toContain("Miftah does not run provider OAuth"); + }); + + it("keeps a practical Claude Desktop path and routes detailed material to the docs", () => { + expect(readme).toContain("miftah init github --preset github"); + expect(readme).toContain("[Claude Desktop setup](docs/claude-desktop.md)"); + expect(readme).toContain("[Configuration reference](docs/config.md)"); + expect(readme).toContain("[Security boundary](docs/security.md)"); + }); +}); From 0d603116ddb943d95a85253981fca4250a807e23 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sat, 18 Jul 2026 20:27:21 +0400 Subject: [PATCH 2/2] fix(secrets): kill orphaned descendants immediately --- src/secrets/secret-process-runner.ts | 8 +++++ tests/fixtures/fake-secret-provider.mjs | 15 +++++++-- tests/secret-providers.test.ts | 44 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/secrets/secret-process-runner.ts b/src/secrets/secret-process-runner.ts index b523d88e..4357d13c 100644 --- a/src/secrets/secret-process-runner.ts +++ b/src/secrets/secret-process-runner.ts @@ -114,6 +114,7 @@ function runPreparedSecretCommand( let forceKill: NodeJS.Timeout | undefined; let terminationPoll: NodeJS.Timeout | undefined; let childClosed = false; + let directChildExited = child.exitCode !== null || child.signalCode !== null; let terminationComplete = true; let finished = false; @@ -137,6 +138,10 @@ function runPreparedSecretCommand( const terminatePosixProcessGroup = (): Promise => { const pid = child.pid; if (pid === undefined) return Promise.resolve(); + if (directChildExited) { + signalPosixProcessGroup(pid, "SIGKILL"); + return Promise.resolve(); + } signalPosixProcessGroup(pid, "SIGTERM"); return new Promise((resolve) => { const complete = () => { @@ -207,6 +212,9 @@ function runPreparedSecretCommand( stderrBytes += retained.length; stderrChunks.push(retained); }); + child.on("exit", () => { + directChildExited = true; + }); child.on("error", () => { terminalKind ??= "unavailable"; }); diff --git a/tests/fixtures/fake-secret-provider.mjs b/tests/fixtures/fake-secret-provider.mjs index db603f65..3c319515 100755 --- a/tests/fixtures/fake-secret-provider.mjs +++ b/tests/fixtures/fake-secret-provider.mjs @@ -24,7 +24,18 @@ async function writeRecord(descendantPid) { } async function spawnDescendant() { - const descendant = spawn(process.execPath, ["-e", "setTimeout(() => {}, 5_000)"], { stdio: "inherit" }); + const descendantScript = + mode === "early-exit-stubborn-descendant" + ? [ + 'const { writeFileSync } = require("node:fs");', + 'const readyPath = process.env.MIFTAH_FAKE_DESCENDANT_READY_PATH;', + 'const signalPath = process.env.MIFTAH_FAKE_DESCENDANT_SIGNAL_PATH;', + 'if (readyPath) writeFileSync(readyPath, "ready");', + 'process.on("SIGTERM", () => { if (signalPath) writeFileSync(signalPath, "SIGTERM"); });', + "setTimeout(() => {}, 5_000);" + ].join("") + : "setTimeout(() => {}, 5_000)"; + const descendant = spawn(process.execPath, ["-e", descendantScript], { stdio: "inherit" }); if (descendant.pid === undefined) throw new Error("Fake descendant did not start"); await writeRecord(descendant.pid); } @@ -44,7 +55,7 @@ if (mode === "sleep") { } else if (mode === "descendant" || mode === "slow-descendant") { await spawnDescendant(); await new Promise((resolve) => globalThis.setTimeout(resolve, mode === "slow-descendant" ? 10_000 : 500)); -} else if (mode === "early-exit-descendant") { +} else if (mode === "early-exit-descendant" || mode === "early-exit-stubborn-descendant") { await spawnDescendant(); process.exit(0); } else if (mode === "locked") { diff --git a/tests/secret-providers.test.ts b/tests/secret-providers.test.ts index 630eb581..5c7d9811 100644 --- a/tests/secret-providers.test.ts +++ b/tests/secret-providers.test.ts @@ -1127,6 +1127,50 @@ exit 0`); }); }); + it.runIf(process.platform !== "win32")( + "force-kills a retained descendant after its direct provider exits", + async () => { + await inSandbox(async (directory) => { + const readyPath = join(directory, "descendant-ready"); + const signalPath = join(directory, "descendant-signal"); + const pending = runSecretCommand( + { + executable: process.execPath, + args: [fakeProviderPath], + environment: { + ...fakeProviderEnvironment(directory, "early-exit-stubborn-descendant"), + MIFTAH_FAKE_DESCENDANT_READY_PATH: readyPath, + MIFTAH_FAKE_DESCENDANT_SIGNAL_PATH: signalPath + } + }, + { timeoutMs: 1_000 } + ); + const descendantPid = await readDescendantPid(directory); + + try { + await waitForCondition( + async () => { + try { + return (await readFile(readyPath, "utf8")) === "ready"; + } catch (error) { + if (errorCode(error) === "ENOENT") return false; + throw error; + } + }, + "stubborn descendant to start", + 500 + ); + + await expect(pending).rejects.toEqual(expect.objectContaining>({ kind: "timeout" })); + await expect(readFile(signalPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await waitForProcessExit(descendantPid); + } finally { + await terminateTestProcess(descendantPid); + } + }); + } + ); + it.runIf(process.platform === "win32")( "closes an orphaned descendant when its direct provider process exits", async () => {