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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. The format

### Added

- [#204](https://github.com/mohanagy/miftah/issues/204) Added guided multi-account Google Search Console onboarding through the CLI and local Console, including an explicit opt-in first-success check that resolves only the selected target, verifies audit/policy/identity boundaries before its one declared read-only probe, and cancels with the requesting Console session. The check trusts only the catalog's reviewed launch envelope and fails closed without launching a process after execution-affecting customization. It creates named profiles with an explicit durable default and separate upstream-owned OAuth state directories per generated configuration file and profile, while continuing to keep browser login, token-cache contents, reauthentication, revocation, and account identity verification outside Miftah. A returning provider-owned account can now be added atomically through the reviewed adapter contract in either CLI or Console, with a fresh isolated state directory, a redacted fail-closed lifecycle record, and no token-cache access. It also adds a separate returning-user path for a simple local STDIO environment credential binding: CLI and Console accept a variable name rather than a secret, require every existing profile to share the same narrow binding, enforce multi-profile safeguards, use guarded audited replacement, and refuse remote HTTP, provider-adapter, OAuth, named-upstream, mixed-profile, duplicate-source, and arbitrary-override configurations. It also adds explicitly acknowledged local STDIO setup and client-entry import: literal executable plus bounded argument array, no shell, no secret-shaped values, no generic-command launch during onboarding, read-only default policy, destructive handling for unknown tools, and Windows-only direct absolute `.exe`/`.com` requirements to avoid a command-shell fallback.
- [#204](https://github.com/mohanagy/miftah/issues/204) Added guided multi-account Google Search Console onboarding through the CLI and local Console, including an explicit opt-in first-success check that resolves only the selected target, verifies audit/policy/identity boundaries before its one declared read-only probe, and cancels with the requesting Console session. The check trusts only the catalog's reviewed launch envelope and fails closed without launching a process after execution-affecting customization. It creates named profiles with an explicit durable default and separate upstream-owned OAuth state directories per generated configuration file and profile, while continuing to keep browser login, token-cache contents, reauthentication, revocation, and account identity verification outside Miftah. A returning provider-owned account can now be added atomically through the reviewed adapter contract in either CLI or Console, with a fresh isolated state directory, a redacted fail-closed lifecycle record, and no token-cache access. It also adds a separate returning-user path for a simple local STDIO environment credential binding: CLI and Console accept a variable name rather than a secret, require every existing profile to share the same narrow binding, enforce multi-profile safeguards, use guarded audited replacement, and refuse remote HTTP, provider-adapter, OAuth, named-upstream, mixed-profile, duplicate-source, and arbitrary-override configurations. It also adds explicitly acknowledged local STDIO setup and client-entry import: literal executable plus bounded argument array, no shell, no secret-shaped values, no generic-command launch during onboarding, read-only default policy, destructive handling for unknown tools, and Windows-only direct absolute `.exe`/`.com` requirements to avoid a command-shell fallback. Returning CLI users can now rerun only the selected provider adapter's declared, audited read-only readiness check with `profile test`, receiving a redacted report and nonzero outcome for any non-ready state without an arbitrary tool call, browser handoff, configuration mutation, or provider-cache access.

### Changed

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,16 @@ miftah setup --add-profile \

There is no generic `--verify` here: Miftah does not guess a safe command for an arbitrary upstream. Set the new environment variable in the application environment that launches Miftah, restart or reconnect the MCP client, then use the service's normal safe validation path. The local dashboard offers the same flow under **Add another environment-backed account** when the selected configuration meets this exact boundary.

### Retest a reviewed provider account

For an existing reviewed provider-adapter account, run its declared safe check again without recreating the account or opening OAuth:

```bash
miftah profile test --config ~/.config/miftah/gsc.json --profile google-personal
```

When a configuration has more than one upstream, add `--upstream <name>`. Miftah runs only the adapter's declared read-only probe for that exact profile and returns a redacted status report. It does not run an arbitrary tool or expose provider output. It does not change configuration, read a provider token cache, or open a browser; a non-ready report exits `1` so automation can detect it.

### Change the durable default later

Once two or more profiles already exist, you can choose which one new Miftah sessions start with without adding an account or editing JSON:
Expand All @@ -479,6 +489,7 @@ These are shell commands. Profile switching and identity tools such as `miftah_u
| Validate JSON only | `miftah validate --config service.json` |
| Check secrets, executable, upstream startup, discovery, and shutdown | `miftah doctor --config service.json` |
| Start one profile and verify initialization | `miftah test-profile --config service.json --profile work` |
| Run the reviewed safe check for one provider-backed account | `miftah profile test --config service.json --profile work` |
| Discover one profile's upstream tools | `miftah list-tools --config service.json --profile work` |
| Read redacted audit events | `miftah logs --config service.json` |
| Follow redacted audit events | `miftah logs --config service.json --follow` |
Expand Down
3 changes: 2 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Usage: miftah [command] [options]
```

The root command list includes `serve`, `dashboard`, `console`, `validate`, `doctor`, `schema`, `setup`, `init`, `migrate-config`, `profile set-default`, `connection add|list|status|test`, `auth connect|reauth|disconnect`, `list-tools`, `test-profile`, `logs`, `audit-export`, `audit-verify`, and `version`. With no command, Miftah runs `serve`.
The root command list includes `serve`, `dashboard`, `console`, `validate`, `doctor`, `schema`, `setup`, `init`, `migrate-config`, `profile set-default|test`, `connection add|list|status|test`, `auth connect|reauth|disconnect`, `list-tools`, `test-profile`, `logs`, `audit-export`, `audit-verify`, and `version`. With no command, Miftah runs `serve`.

Documented command names, options, JSON success forms, and exit categories are compatibility contracts. An incompatible CLI removal, rename, required-option change, or semantic output change requires the pre-1.0 deprecation/removal process in the [public compatibility policy](library-api.md#compatibility-policy).

Expand All @@ -28,6 +28,7 @@ Documented command names, options, JSON success forms, and exit categories are c
| `miftah init [name]` | none | `--name <name>`, `--preset <name>`, `--output <file>`, `--interactive`, `--client <claude-desktop\|claude-code\|cursor\|vscode\|all>`, `--credential-env <name>`, `--npm-package <package>`, `--docker-image <image>`, `--url <url>`, `--header-name <name>`, `--header-prefix <prefix>`, `--oauth-client-secrets-file <file>`, `--local-command <executable>`, repeated `--arg <value>`, `--cwd <directory>`, `--accept-local-command` | Writes a strict catalog configuration with exclusive creation and can print client JSON snippets. The positional `name` and `--name` are alternatives; the default name is `miftah-wrapper`. |
| `miftah migrate-config --config <file>` | `--config` | `--config <file>`, `--write` | Plans a supported configuration-format migration and writes a safe JSON report. It is dry-run by default. `--write` validates the candidate, makes an exact exclusive `<file>.bak`, then uses a same-directory non-overwriting publication for a changed regular non-symlink source; it never resolves secrets or starts an upstream. |
| `miftah profile set-default --config <file> --profile <name>` | `--config`, `--profile` | `--config <file>`, `--profile <name>` | Makes one existing profile the durable default for future Miftah sessions. It performs a guarded replacement with a recovery backup and finalizes a configured fail-closed audit record. Its result omits configuration bytes, profile data, provider paths, and secret references; it never starts an upstream, opens OAuth, or changes profile data, provider caches, or an active client session. |
| `miftah profile test --config <file> --profile <name>` | `--config`, `--profile` | `--config <file>`, `--profile <name>`, `--upstream <name>` | Runs only the selected provider adapter's declared read-only readiness check and writes its redacted report. It never accepts an arbitrary tool name, exposes provider output, changes configuration, reads provider token caches, or opens a browser. `--upstream` is required only when the configuration has multiple upstreams. It exits `0` only for `ready`; other bounded readiness results exit `1`. |
| `miftah connection add --config <file>` | `--config`, `--profile`, `--issuer`, `--client-registration` | `--connection <ref>`, `--upstream <name>`, repeated `--scope <scope>`, `--write` | Plans a v3 OAuth binding by default. `--write` applies the reviewed candidate with a unique recovery backup and configured audit event. It never resolves credentials or starts an upstream. |
| `miftah connection list --config <file>` | `--config` | `--client <claude-desktop\|claude-code\|cursor\|vscode\|all>` | Lists redacted connection state. Optional snippets are copyable JSON only; Miftah never edits client settings. |
| `miftah connection status --config <file>` | `--config` plus an unambiguous selector | `--connection <ref>` or `--profile <name>` with optional `--upstream <name>` | Shows exact non-secret binding, credential expiry/state, and coarse identity state. |
Expand Down
11 changes: 11 additions & 0 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { exitCodeForError } from "./exit-codes.js";
import { runLogsCommand } from "./logs.js";
import { runInitCommand } from "./init.js";
import { runSetupCommand } from "./setup.js";
import { runProfileReadinessCommand } from "./profile-readiness-command.js";
import { runDefaultProfileChange } from "../setup/profile-default-onboarding.js";
import { runAuditExportCommand } from "./audit-export.js";
import { formatAuditVerifyReport, runAuditVerifyCommand } from "./audit-verify.js";
Expand Down Expand Up @@ -224,6 +225,16 @@ async function main(argv = process.argv.slice(2)): Promise<void> {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return;
}
if (command === "profile test") {
const result = await runProfileReadinessCommand({
configPath: args.config,
profile: requireOption(command, "profile", args.profile),
...(args.upstream === undefined ? {} : { upstream: args.upstream })
});
process.stdout.write(`${JSON.stringify(result.report, null, 2)}\n`);
process.exitCode = result.exitCode;
return;
}
if (command === "connection add") {
const report = await runConnectionAddCommand({
configPath: args.config,
Expand Down
4 changes: 4 additions & 0 deletions src/cli/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ export const CLI_COMMANDS = {
description: "Change the durable default profile for future MCP sessions.",
options: ["config", "profile"]
},
"profile test": {
description: "Run one reviewed safe readiness check for an explicit account profile.",
options: ["config", "profile", "upstream"]
},
"list-tools": {
description: "List tools available for a profile.",
options: ["config", "profile"]
Expand Down
23 changes: 23 additions & 0 deletions src/cli/profile-readiness-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { runProfileReadiness, type ProfileReadinessReport } from "../setup/profile-readiness.js";

export interface ProfileReadinessCommandOptions {
readonly configPath: string;
readonly profile: string;
readonly upstream?: string;
}

export interface ProfileReadinessCommandResult {
readonly report: ProfileReadinessReport;
readonly exitCode: 0 | 1;
}

/** Runs only the provider-declared reviewed readiness check for one explicit account profile. */
export async function runProfileReadinessCommand(
options: ProfileReadinessCommandOptions
): Promise<ProfileReadinessCommandResult> {
const report = await runProfileReadiness(options.configPath, {
profile: options.profile,
...(options.upstream === undefined ? {} : { upstream: options.upstream })
});
return { report, exitCode: report.status === "ready" ? 0 : 1 };
}
19 changes: 19 additions & 0 deletions tests/cli-parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,25 @@ describe("CLI parser", () => {
});
});

it("parses an explicit provider-declared profile readiness check", () => {
expect(parseCli([
"profile",
"test",
"--config=/Users/example/.config/miftah/gsc.json",
"--profile=google-personal",
"--upstream=default"
])).toEqual({
kind: "run",
command: "profile test",
options: {
config: "/Users/example/.config/miftah/gsc.json",
profile: "google-personal",
upstream: "default"
}
});
expect(renderCommandHelp("profile test")).toContain("reviewed safe readiness check");
});

it("parses all init-only onboarding options before or after init, including equals values", () => {
expect(
parseCli([
Expand Down
33 changes: 33 additions & 0 deletions tests/package-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,31 @@ describe("packed artifact contract", () => {
await consoleServe.stop();
}

const unsupportedProfileReadiness = runInstalledBinary(
binary,
["profile", "test", "--config", httpServeConfigPath, "--profile", "work"],
cliContractDirectory
);
expect(unsupportedProfileReadiness.status, unsupportedProfileReadiness.stderr || unsupportedProfileReadiness.stdout).toBe(1);
expect(unsupportedProfileReadiness.stderr).toBe("");
expect(JSON.parse(unsupportedProfileReadiness.stdout)).toEqual({
status: "unsupported",
profile: "work",
upstream: "default",
safeRead: { status: "unavailable", errorCode: "PROFILE_READINESS_UNSUPPORTED" },
identity: { status: "not-checked" }
});
const profileReadinessWithoutExplicitProfile = runInstalledBinary(
binary,
["profile", "test", "--config", httpServeConfigPath],
cliContractDirectory
);
expect(profileReadinessWithoutExplicitProfile.status).toBe(2);
expect(profileReadinessWithoutExplicitProfile.stdout).toBe("");
expect(profileReadinessWithoutExplicitProfile.stderr).toContain(
"Command 'profile test' requires '--profile <value>'"
);

const dashboardConfigPath = join(cliContractDirectory, "first dashboard config.json");
const dashboardServe = await startInstalledCli(
installedCliEntry,
Expand All @@ -1355,6 +1380,14 @@ describe("packed artifact contract", () => {
expect(rootHelp.status, rootHelp.stderr || rootHelp.stdout).toBe(0);
expect(rootHelp.stderr).toBe("");
expect(rootHelp.stdout).toContain("Usage: miftah [command] [options]");
expect(rootHelp.stdout).toContain("profile test");
const profileTestHelp = runInstalledBinary(binary, ["profile", "test", "--help"], cliContractDirectory);
expect(profileTestHelp.status, profileTestHelp.stderr || profileTestHelp.stdout).toBe(0);
expect(profileTestHelp.stderr).toBe("");
expect(profileTestHelp.stdout).toContain("Usage: miftah profile test");
expect(profileTestHelp.stdout).toContain("--config <file>");
expect(profileTestHelp.stdout).toContain("--profile <name>");
expect(profileTestHelp.stdout).toContain("--upstream <name>");
const commandOptions = {
serve: ["--config <file>"],
console: ["--config <file>", "--port <number>"],
Expand Down
58 changes: 58 additions & 0 deletions tests/profile-readiness-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const profileReadinessMocks = vi.hoisted(() => ({ run: vi.fn() }));

vi.mock("../src/setup/profile-readiness.js", () => ({
runProfileReadiness: profileReadinessMocks.run
}));

import { runProfileReadinessCommand } from "../src/cli/profile-readiness-command.js";

describe("profile readiness command", () => {
beforeEach(() => {
profileReadinessMocks.run.mockReset();
});

it("uses one explicitly selected profile and returns the redacted ready report", async () => {
const report = {
status: "ready" as const,
profile: "google-personal",
upstream: "default",
adapter: "Google Search Console",
safeRead: { status: "passed" as const, tool: "get_capabilities" },
identity: { status: "unavailable" as const }
};
profileReadinessMocks.run.mockResolvedValue(report);

await expect(runProfileReadinessCommand({
configPath: "/Users/example/.config/miftah/gsc.json",
profile: "google-personal",
upstream: "default"
})).resolves.toEqual({ report, exitCode: 0 });
expect(profileReadinessMocks.run).toHaveBeenCalledWith(
"/Users/example/.config/miftah/gsc.json",
{ profile: "google-personal", upstream: "default" }
);
});

it("returns a nonzero result for a supported command whose selected profile is not ready", async () => {
const report = {
status: "blocked" as const,
profile: "google-personal",
upstream: "default",
adapter: "Google Search Console",
safeRead: { status: "blocked" as const, tool: "get_capabilities", errorCode: "POLICY_BLOCKED" },
identity: { status: "not-checked" as const }
};
profileReadinessMocks.run.mockResolvedValue(report);

await expect(runProfileReadinessCommand({
configPath: "/Users/example/.config/miftah/gsc.json",
profile: "google-personal"
})).resolves.toEqual({ report, exitCode: 1 });
expect(profileReadinessMocks.run).toHaveBeenCalledWith(
"/Users/example/.config/miftah/gsc.json",
{ profile: "google-personal" }
);
});
});
3 changes: 3 additions & 0 deletions tests/readme-product-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ describe("product README", () => {
expect(readme).toContain("--credential-env SENTRY_PERSONAL_ACCESS_TOKEN");
expect(readme).toContain("Remote HTTP MCPs are refused because profile environments do not authenticate HTTP requests.");
expect(readme).toContain("Add another environment-backed account");
expect(readme).toContain("### Retest a reviewed provider account");
expect(readme).toContain("miftah profile test --config ~/.config/miftah/gsc.json --profile google-personal");
expect(readme).toContain("It does not run an arbitrary tool or expose provider output.");
expect(readme).toContain("### Change the durable default later");
expect(readme).toContain("miftah profile set-default --config ~/.config/miftah/gsc.json --profile google-personal");
expect(readme).toContain("does not switch an already-running MCP client");
Expand Down