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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. The format
- [#18](https://github.com/mohanagy/miftah/issues/18) The packed-package contract now exercises the installed CLI through shell and Windows command quoting, paths with spaces, generated help, stable category exits, JSON automation output, and normalized/redacted audit-log output.
- [#18](https://github.com/mohanagy/miftah/issues/18) The CLI reference now documents generated help, every command and option, version compatibility output, exit statuses, JSON streams, and audit snapshot/follow safety boundaries.
- [#19](https://github.com/mohanagy/miftah/issues/19) The versioned strict preset catalog, first-run onboarding wizard, generated absolute client snippets, compatibility matrix, and exact generated examples are documented and contract-tested. Tests validate generated configuration without constructing or starting external providers.
- [#20](https://github.com/mohanagy/miftah/issues/20) Metadata-only routing context now combines bounded workspace signals with deterministic profile selection, capability-gated MCP roots, strict project markers, and sanitized route-preview/audit evidence.

### Changed

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ Routing can use the active profile or rules matching tool arguments:

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 prompt retrieval. Policy patterns use each upstream tool's original name for tools, `resources/read` for reads, and `prompts/get` for prompt retrieval. A deny, confirmation-required, blocked, or ambiguous decision is returned before Miftah forwards the read or prompt request. 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.

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, not raw project environment values or project file contents. See [routing context](docs/config.md#routing-context) for the marker schema, root behavior, and evidence boundary.

## Secret handling

Supported local references include environment variables (`${NAME}` and `secretref:env://NAME`), configured dotenv files (`secretref:dotenv://NAME`), and explicitly opt-in plaintext (`secretref:plain://...`). OS keychains and external secret CLIs are reserved extension points. Secrets are redacted from diagnostics, errors, stderr forwarding, audit entries, and tool responses.
Expand All @@ -111,7 +113,7 @@ Use `miftah doctor` to inspect config and upstream readiness without printing pr

## Audit logging

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. Wrapper and upstream lifecycle transitions are recorded separately. Arguments are omitted unless `audit.includeArguments` is `true`.
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`.

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.

Expand Down
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Per-profile start and restart single-flight state, attempt tokens, and lifecycle

The server advertises management tools plus tools discovered from the active profile. It advertises `tools.listChanged` and emits `notifications/tools/list_changed` after profile changes, restarts, and recovery that changes the public tool snapshot. Unknown names are rejected from the capability snapshot rather than guessed or forwarded. If a routing rule targets another profile, Miftah forwards only when that profile exposes the same name with an identical client-visible schema; otherwise it returns `TOOL_SCHEMA_MISMATCH`. In strict discovery mode, Miftah preflights every configured profile and rejects unavailable upstreams or any mismatched exposed tool contract before publishing a snapshot.

Every supported MCP request enters one outer audit scope. The scope records one terminal operation event on success or safe failure and also covers discovery/list failures, unknown names, and management tools that do not enter the proxy pipeline. `OperationPipeline` enriches proxied tool calls, resource reads, and prompt retrieval with captured source/target profile, upstream, routing, policy, and risk metadata; it does not emit its own record. It captures the source profile state before awaiting work, resolves routing against that fixed active-profile fallback, evaluates the selected profile policy, resolves the exact target upstream route, executes, and redacts the result or error. Tools retain their original upstream names for routing and policy compatibility; resource reads and prompt retrieval use the stable policy names `resources/read` and `prompts/get`. Denied, confirmation-required, blocked, and ambiguous operations never resolve or execute an upstream read/get route. Upstream managers publish typed lifecycle transitions, which the server records as separate audit events without letting audit I/O interrupt cleanup or recovery.
The metadata-only routing context collector is the sole source of workspace context, profile hints, preview evidence, and proxied-operation audit evidence. It reads only allowlisted bounded metadata: file roots, cwd, two environment selectors, strict project markers, package/workspace fields, and a local Git origin. The runtime configuration file remains separate from that input. Once initialized, `MiftahServer` capability-gates `roots/list`, caches only root URIs per client connection, and refreshes only on an advertised roots-list-changed notification. Every proxied operation and route preview receives one immutable snapshot; it uses that snapshot for rule matching, hint selection, and redacted evidence so routing cannot change between those steps.

Every supported MCP request enters one outer audit scope. The scope records one terminal operation event on success or safe failure and also covers discovery/list failures, unknown names, and management tools that do not enter the proxy pipeline. `OperationPipeline` enriches proxied tool calls, resource reads, and prompt retrieval with captured source/target profile, upstream, routing, policy, risk, and sanitized `routingEvidence`; it does not emit its own record. It captures the source profile state before awaiting work, resolves routing against that fixed active-profile fallback, evaluates the selected profile policy, resolves the exact target upstream route, executes, and redacts the result or error. Route preview uses the same captured fallback and collector contract. Tools retain their original upstream names for routing and policy compatibility; resource reads and prompt retrieval use the stable policy names `resources/read` and `prompts/get`. Denied, confirmation-required, blocked, and ambiguous operations never resolve or execute an upstream read/get route. Upstream managers publish typed lifecycle transitions, which the server records as separate audit events without letting audit I/O interrupt cleanup or recovery.

For a multi-entry `upstreams` map, `ResourcePromptRegistry` discovers resources and prompts from every configured upstream and publishes namespaced public values only after collision checks succeed. It names resources and prompts `<upstream>__<name>`, and exposes resources as `miftah://resource/<encoded-upstream>?uri=<encoded-redacted-upstream-uri>`. The registry retains the original upstream URI privately with the exact profile and upstream route. Prompt resource links and read-result sub-resources are registered as exact Miftah routes to their originating upstream. Before resource/prompt URI metadata crosses the boundary, structural redaction strips userinfo/fragments and redacts query values, including URI metadata returned by reads and prompt content. After the operation pipeline authorizes its selected profile, a read or prompt get resolves that exact route and forwards only to its originating upstream; unknown identifiers are rejected rather than forwarded. Aggregate pagination stores the individual upstream cursors behind opaque, bounded in-memory LRU state scoped to the profile and capability kind.

Expand Down
32 changes: 31 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ The supported routing fallback values are:
- `ask`: return `ROUTING_AMBIGUOUS` when no unique rule applies.
- `block`: reject requests that do not match a rule.

## Routing context

The explicit `--config` file is the only runtime configuration authority. Miftah never auto-loads a second configuration file. It treats a discovered `.miftahrc.json` or non-runtime `miftah.json` as a metadata-only project marker only when it has this exact shape:

```json
{
"profiles": {
"<wrapper-name>": "<profile-name>"
}
}
```

The marker's only top-level key must be `profiles`, and all values are strings. It may contain mappings for other wrapper names, but only `<wrapper-name>` is selected; that name is the trusted runtime configuration's `name`, and its `<profile-name>` must name one of the configured profiles. A marker cannot configure an upstream, policy, header, environment value, audit setting, secret source, or any other Miftah option; it does not merge with runtime configuration. Miftah reads only bounded known metadata files while walking from the process working directory to an applicable local boundary and ignores malformed, irrelevant, oversized, or out-of-boundary files. The nearest valid project marker wins; in one directory, `.miftahrc.json` is checked before `miftah.json`, and markers are not combined.

The metadata-only collector can provide rules with `context.*` values from:

- validated MCP client `file:` roots and the process working directory;
- `MIFTAH_PROFILE` and `MIFTAH_PROJECT`;
- the strict project marker above;
- bounded `package.json` name/repository and workspace metadata;
- the local Git `remote.origin.url`.

`MIFTAH_PROFILE` must name a configured profile or routing fails closed. `MIFTAH_PROJECT` is contextual metadata only: Miftah does not interpret it as a path or probe the filesystem from it. URI-like project and repository values, Git origins, and roots are structurally sanitized before evidence crosses an MCP or audit boundary.

Selection order is environment hint, project-marker hint, configured rule, then fallback. The marker stage is the one nearest valid marker above; matching rules that select different profiles return `ROUTING_AMBIGUOUS`, and Miftah does not guess. Context and profile hints do not replace the explicit-rule requirement for a destructive operation when `security.requireExplicitProfileForDestructive` is enabled.

MCP roots are optional client metadata. After initialization, Miftah calls `roots/list` only when the client advertises `roots` capability, stores a URI-only snapshot for that connection, and refreshes it only on an advertised `notifications/roots/list_changed`. An unsupported or failed roots request yields an empty-root snapshot; Miftah does not poll roots or request them for every operation.

`miftah_route_preview` resolves against one collector snapshot using the same context inputs as a proxied operation. Its response contains the selected profile, reason, policy decision, and sanitized `evidence`. Proxied operation audit records carry the same snapshot as additive `routingEvidence`, including an ambiguity that prevents forwarding. Evidence contains only allowlisted metadata and redacted URI components; it never contains arbitrary project file content or the raw `MIFTAH_PROJECT` value.

## Operation routing and policy

Miftah applies one safety pipeline to every proxied upstream tool call, resource read, and prompt retrieval. It captures the active profile once at request start, resolves the routing rule using that immutable fallback, evaluates the selected profile's policy before resolving an aggregate route or forwarding, redacts the result or error, and records the terminal operation metadata in audit output.
Expand All @@ -62,7 +92,7 @@ Routing rules receive a tool's original arguments unchanged. Resource reads expo

Policies classify these operation names as `read`, `write`, or `destructive` using configurable overrides and conservative name heuristics. `denyRisk` takes precedence over `allowRisk`; `requireConfirmation` returns a structured error instead of forwarding the operation.

Audit logging writes local JSONL when a path is configured. Every supported MCP request emits one terminal operation event with a request ID, per-process session ID, source/selected profiles, stable outcome/error code, duration, and any available upstream, routing, policy, and risk metadata; wrapper and upstream lifecycle transitions emit separate event records. Arguments are excluded unless `includeArguments` is true, and all configured secret values are redacted before writing. Audit directories and files are created with owner-only permissions where the platform supports them.
Audit logging writes local JSONL when a path is configured. Every supported MCP request emits one terminal operation event with a request ID, per-process session ID, source/selected profiles, stable outcome/error code, duration, and any available upstream, routing, policy, and risk metadata; route previews and proxied operations add sanitized `routingEvidence` when a collector snapshot is available. Wrapper and upstream lifecycle transitions emit separate event records. Arguments are excluded unless `includeArguments` is true, and all configured secret values are redacted before writing. Audit directories and files are created with owner-only permissions where the platform supports them.

`audit.failureMode` accepts `"fail-closed"` (the default) or `"fail-open"`. Fail-closed verifies the configured sink before dispatch and refuses a request when the sink cannot be prepared; a terminal write error also surfaces as `AUDIT_WRITE_FAILED`. Because terminal writes occur after an upstream operation, that error can leave a non-idempotent operation's outcome indeterminate; do not blindly retry it. Fail-open preserves the request result and exposes a redacted `AUDIT_WRITE_FAILED` entry through `miftah_health`; it should be used only when availability is more important than complete auditability.

Expand Down
4 changes: 4 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Miftah is a credential broker, so safe defaults are part of the product contract

Audit writes default to fail-closed: Miftah verifies the configured sink before dispatch and refuses a request when the sink cannot be prepared. A terminal write can fail after an upstream side effect has completed, so a post-dispatch `AUDIT_WRITE_FAILED` has an indeterminate outcome and must not prompt a blind retry of a non-idempotent operation. An operator can set `audit.failureMode` to `"fail-open"` for availability-sensitive deployments; Miftah then preserves the request outcome but exposes a redacted `AUDIT_WRITE_FAILED` health entry. This mode trades complete auditability for availability.

The explicit runtime configuration is trusted operator input; workspace routing metadata is not. Project markers cannot inject configuration because their only accepted shape maps the configured wrapper name to an already-known profile. They cannot add credentials, environment variables, headers, upstreams, policies, audit controls, or secret references. Miftah reads only named, bounded metadata files within the applicable working-directory/root boundary and does not scan arbitrary project content.

Routing evidence is deliberately narrower than routing context. It is passed through audit redaction before it reaches a client or JSONL record, strips URI userinfo/fragments and redacts URI query values, and never contains the raw `MIFTAH_PROJECT` value or arbitrary project file content. An unrecognized environment profile or ambiguous matching rules fails closed instead of selecting an account; standard project-marker discovery deterministically uses the nearest valid marker. Profile hints also cannot satisfy an explicit-rule requirement for destructive operations.

Miftah cannot reduce privileges granted by a provider token. A read-only Miftah policy is a local blocklist, not a replacement for provider-side scopes. Avoid putting real credentials in examples, commits, or support logs.

`init` never asks for or echoes a secret value: its wizard accepts variable names and validated endpoint/header metadata only. Generated provider examples contain `${ENV_NAME}` references, and client snippets are printed for explicit copy rather than written into host configuration files. For exact preset pins, GitHub tag-to-digest recording guidance, Sentry skill-filtering limits, and client locations, see [preset and client compatibility](presets-and-clients.md).
Expand Down
3 changes: 3 additions & 0 deletions src/audit/audit-trail.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import { AuditLogger } from "./audit-logger.js";
import type { AuditEvent, AuditHealth, AuditRoutingSource, AuditStatus } from "./audit-types.js";
import type { RoutingContextEvidence } from "../routing/routing-types.js";

export interface AuditOperationInput {
operation: string;
Expand All @@ -19,6 +20,7 @@ export interface AuditScopeUpdate {
policyName?: string;
policyDecision?: AuditEvent["policyDecision"];
risk?: AuditEvent["risk"];
routingEvidence?: RoutingContextEvidence;
}

export interface AuditScopeResult {
Expand Down Expand Up @@ -133,6 +135,7 @@ export class AuditScope {
...(this.event.policyName === undefined ? {} : { policyName: this.event.policyName }),
...(this.event.policyDecision === undefined ? {} : { policyDecision: this.event.policyDecision }),
...(this.event.risk === undefined ? {} : { risk: this.event.risk }),
...(this.event.routingEvidence === undefined ? {} : { routingEvidence: this.event.routingEvidence }),
...(this.event.arguments === undefined ? {} : { arguments: this.event.arguments }),
...(result.errorCode === undefined ? {} : { errorCode: result.errorCode })
});
Expand Down
2 changes: 2 additions & 0 deletions src/audit/audit-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PolicyAction, RiskLevel } from "../policy/policy-types.js";
import type { RoutingContextEvidence } from "../routing/routing-types.js";

export type AuditFailureMode = "fail-open" | "fail-closed";

Expand Down Expand Up @@ -36,6 +37,7 @@ export interface AuditEvent {
policyName?: string;
policyDecision?: PolicyAction;
risk?: RiskLevel;
routingEvidence?: RoutingContextEvidence;
arguments?: unknown;
errorCode?: string;
}
Loading
Loading