@@ -462,7 +467,11 @@ p { color: var(--muted); line-height: 1.6; }
.gate { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, 1fr); gap: clamp(2rem, 7vw, 6rem); border-top: 1px solid var(--line); padding: 2rem 0 4rem; }
.gate h2 { max-width: 31rem; }
.field-note { margin: .65rem 0 0; font-size: .8rem; }
-.intro, .work-section { border-top: 1px solid var(--line); padding: 2rem 0 clamp(3rem, 7vw, 6rem); }
+.work-section { border-top: 1px solid var(--line); padding: 2rem 0 clamp(3rem, 7vw, 6rem); }
+.authentication-guide { margin: 0 0 clamp(3rem, 7vw, 6rem); padding: 0; border: 1px solid var(--line); background: rgb(255 255 255 / 2%); }
+.authentication-guide > summary { padding: 1.15rem 1.25rem; color: var(--ink); }
+.authentication-guide[open] > summary { border-bottom: 1px solid var(--line); }
+.authentication-guide-body { padding: 1.5rem 1.25rem 0; }
.mode-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin-top: 2rem; background: var(--line); border: 1px solid var(--line); }
.mode { min-height: 14rem; padding: 1.3rem; background: var(--panel); }
.mode-native { box-shadow: inset 0 .2rem 0 var(--key); }
@@ -508,7 +517,7 @@ button.danger { color: #ffd7cf; background: transparent; border: 1px solid #7043
.setup-review p { margin: 0 0 .65rem; }
.setup-review ul { display: grid; gap: .35rem; margin: .8rem 0 1rem; padding-left: 1.25rem; color: var(--muted); }
.setup-review .form-action { justify-content: flex-start; flex-wrap: wrap; gap: .6rem; }
-.summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); }
+.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); }
.summary article { display: flex; min-height: 9rem; flex-direction: column; gap: .45rem; padding: 1.25rem; background: var(--panel); }
.summary strong { font: 500 1.5rem/1.15 Georgia, serif; }
.summary span { color: var(--muted); font-size: .8rem; line-height: 1.45; }
@@ -521,9 +530,15 @@ button.danger { color: #ffd7cf; background: transparent; border: 1px solid #7043
.configuration-catalog-status ul:empty { display: none; }
.catalog-actions { display: grid; gap: .8rem; justify-items: start; }
.catalog-actions p { margin: 0; }
+.catalog-actions strong { color: var(--ink); }
+.catalog-boundary { margin-top: 1rem; }
.configuration-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1rem; align-items: center; padding: 1.15rem 1.25rem; border: 1px solid var(--line); background: var(--panel); }
.configuration-card p { margin: .25rem 0 0; font-size: .82rem; }
.configuration-card .configuration-meta { font: .73rem/1.5 ui-monospace, monospace; }
+.configuration-profiles { display: flex; flex-wrap: wrap; gap: .4rem; margin: .75rem 0 0; padding: 0; list-style: none; }
+.configuration-profiles li { padding: .28rem .5rem; color: var(--ink); background: var(--ground); border: 1px solid var(--line); font: .72rem/1.35 ui-monospace, monospace; }
+.configuration-profiles .configuration-default { border-color: var(--key); }
+.configuration-card .configuration-switch { margin-top: .7rem; color: var(--ink); }
.configuration-card button { min-height: 2.4rem; font-size: .78rem; }
.provider-authentication { border-left: .2rem solid var(--safe); padding-left: 1.2rem; background: linear-gradient(90deg, rgb(117 201 154 / 7%), transparent 50%); }
.provider-authentication .section-heading { margin-bottom: 0; }
@@ -621,6 +636,7 @@ const script = `(() => {
let activeSetupDraft = undefined;
let returningSetupVisible = false;
let setupWizardSource = "connector";
+ let profileSwitchingFromMcp = false;
function message(text) {
if (status) status.textContent = text;
@@ -976,7 +992,23 @@ const script = `(() => {
const summary = document.createElement("p");
const profileCount = typeof configuration.profileCount === "number" ? configuration.profileCount : 0;
const defaultProfile = typeof configuration.defaultProfile === "string" ? configuration.defaultProfile : "unknown";
- summary.textContent = profileCount + " profile" + (profileCount === 1 ? "" : "s") + " · default " + defaultProfile;
+ summary.textContent = "Default for new connections: " + defaultProfile;
+ const configuredProfileNames = Array.isArray(configuration.profileNames)
+ ? configuration.profileNames.filter((profile) => typeof profile === "string")
+ : [];
+ const profileNames = configuredProfileNames.length > 0
+ ? configuredProfileNames
+ : defaultProfile === "unknown" ? [] : [defaultProfile];
+ const profileList = document.createElement("ul");
+ profileList.className = "configuration-profiles";
+ const visibleProfileCount = profileNames.length > 0 ? profileNames.length : profileCount;
+ profileList.setAttribute("aria-label", visibleProfileCount + " named account profile" + (visibleProfileCount === 1 ? "" : "s"));
+ profileNames.forEach((profile) => {
+ const item = document.createElement("li");
+ item.textContent = profile + (profile === defaultProfile ? " · default" : "");
+ if (profile === defaultProfile) item.className = "configuration-default";
+ profileList.append(item);
+ });
const ownership = document.createElement("p");
ownership.className = "configuration-meta";
const authentication = record(configuration.authentication);
@@ -985,12 +1017,17 @@ const script = `(() => {
: authentication.mode === "miftah-native-oauth"
? "Miftah-managed native OAuth available"
: "manual upstream authentication required";
- details.append(title, summary, ownership);
+ const switching = document.createElement("p");
+ switching.className = "configuration-switch";
+ switching.textContent = configuration.profileSwitchingFromMcp === true
+ ? "Live account switch: use miftah_use_profile in your MCP client."
+ : "Live account switch: off. Open a new connection to use another default.";
+ details.append(title, summary, profileList, switching, ownership);
const button = document.createElement("button");
button.type = "button";
button.dataset.configuration = id;
const selected = id.length > 0 && id === catalog.selectedConfigurationId;
- button.textContent = selected ? "Open now" : "Open configuration";
+ button.textContent = selected ? "Open now" : "Manage connection";
button.className = selected ? "secondary" : "";
button.disabled = !id || selected;
card.append(details, button);
@@ -1738,9 +1775,24 @@ const script = `(() => {
const configVersion = byId("config-version");
const configuredDefaultProfile = typeof metadata.defaultProfile === "string" ? metadata.defaultProfile : "";
const defaultProfile = byId("default-profile");
+ const profileSwitchingState = byId("profile-switching-state");
+ const profileSwitchingCopy = byId("profile-switching-copy");
+ const activeProfileGuidance = byId("active-profile-guidance");
+ profileSwitchingFromMcp = metadata.profileSwitchingFromMcp === true;
if (configName) configName.textContent = String(metadata.name || "—");
if (configVersion) configVersion.textContent = "Config v" + String(metadata.version || "—");
if (defaultProfile) defaultProfile.textContent = configuredDefaultProfile || "—";
+ if (profileSwitchingState) profileSwitchingState.textContent = profileSwitchingFromMcp ? "Available" : "Off";
+ if (profileSwitchingCopy) {
+ profileSwitchingCopy.textContent = profileSwitchingFromMcp
+ ? "Use miftah_use_profile in your MCP client to switch this active session."
+ : "Open a new connection after changing the durable default.";
+ }
+ if (activeProfileGuidance) {
+ activeProfileGuidance.textContent = profileSwitchingFromMcp
+ ? "Active vs durable: changing the default affects new connections. Existing sessions keep their account until you use miftah_use_profile in the MCP client."
+ : "Active vs durable: changing the default affects new connections. Existing sessions keep their account until you reconnect.";
+ }
const profileMetadata = Array.isArray(metadata.profiles) ? metadata.profiles.map(record) : [];
const profiles = profileMetadata.map((item) => String(item.name || "")).filter(Boolean);
const upstreams = Array.isArray(metadata.upstreams) ? metadata.upstreams.map((item) => String(record(item).name || "")).filter(Boolean) : [];
@@ -1766,7 +1818,9 @@ const script = `(() => {
if (auditState) auditState.textContent = String(audit.state || "unknown");
renderConnections(results[1], metadata.authentication);
renderAudit(results[2]);
- message("Console data refreshed. Existing MCP clients still need a restart for durable changes.");
+ message(profileSwitchingFromMcp
+ ? "Console data refreshed. Durable changes apply to new connections; use miftah_use_profile for this active MCP session."
+ : "Console data refreshed. Open a new MCP connection before expecting durable changes to be active.");
}
if (unlockForm instanceof HTMLFormElement && bootstrapInput instanceof HTMLInputElement) {
@@ -2242,7 +2296,10 @@ const script = `(() => {
: "This profile is already the durable default.";
if (result) result.textContent = publicResult;
await refresh();
- message(publicResult + " Existing MCP clients need a restart; if you are using the configuration catalog, select this configuration again before another Console change.");
+ message(publicResult + (profileSwitchingFromMcp
+ ? " New connections will use it; use miftah_use_profile to switch this active MCP session."
+ : " Open a new MCP connection to use it.") +
+ " If you are using the configuration catalog, select this configuration again before another Console change.");
} catch (error) { message(errorMessage(error)); }
finally { setDefaultProfile.disabled = false; }
});
diff --git a/src/console/console-config-catalog.ts b/src/console/console-config-catalog.ts
index f557d9ad..869f9c86 100644
--- a/src/console/console-config-catalog.ts
+++ b/src/console/console-config-catalog.ts
@@ -586,7 +586,9 @@ export async function discoverConsoleConfigCatalog(
name: summary.name,
version: summary.version,
profileCount: summary.profiles.length,
+ profileNames: summary.profiles.map(({ name }) => name),
defaultProfile: summary.defaultProfile,
+ profileSwitchingFromMcp: summary.profileSwitchingFromMcp === true,
authentication: summary.authentication ?? {
mode: "miftah-native-oauth",
credentialOwner: "miftah",
diff --git a/src/console/console-config-metadata.ts b/src/console/console-config-metadata.ts
index bee33ee4..708c860f 100644
--- a/src/console/console-config-metadata.ts
+++ b/src/console/console-config-metadata.ts
@@ -45,7 +45,11 @@ export interface ConsoleDiscoveredConfiguration {
readonly name: string;
readonly version: string;
readonly profileCount: number;
+ /** Non-secret profile names shown before a configuration is selected. */
+ readonly profileNames?: readonly string[];
readonly defaultProfile: string;
+ /** Whether the MCP client may switch the active session with `miftah_use_profile`. */
+ readonly profileSwitchingFromMcp?: boolean;
readonly authentication: ConsoleAuthenticationMetadata;
readonly source: "standard-config-directory";
}
@@ -85,6 +89,8 @@ export interface ConsoleInitializedConfigMetadata {
readonly profiles: readonly ProfileInventoryEntry[];
readonly upstreams: readonly { readonly name: string; readonly transport: string }[];
readonly oauthConnectionCount: number;
+ /** Whether the MCP client may switch the active session with `miftah_use_profile`. */
+ readonly profileSwitchingFromMcp?: boolean;
/** Present for live Console services; optional for embedding compatibility. */
readonly authentication?: ConsoleAuthenticationMetadata;
/** Present only for a no-config dashboard invocation. */
@@ -195,6 +201,7 @@ export function consoleInitializedConfigMetadata(config: MiftahConfig): ConsoleI
profiles: inventory.profiles,
upstreams: upstreams.map(({ name, transport }) => ({ name, transport })),
oauthConnectionCount: config.version === "3" ? Object.keys(config.oauth?.connections ?? {}).length : 0,
+ profileSwitchingFromMcp: config.security?.allowProfileSwitchingFromMcp === true,
authentication: consoleAuthenticationMetadata(config),
restartRequiredForExistingClients: true
};
diff --git a/tests/config-migration.test.ts b/tests/config-migration.test.ts
index ea0d15ea..d620d900 100644
--- a/tests/config-migration.test.ts
+++ b/tests/config-migration.test.ts
@@ -194,12 +194,21 @@ vi.mock("../src/cli/windows-config-acl.js", async (importOriginal) => {
...args: Parameters
): Promise => {
if (migrationRace.timeoutDiagnosticActive) migrationRace.timeoutDiagnosticPhase = "transaction-create";
+ if (process.platform === "win32") {
+ const [directory] = args;
+ const { mkdir } = await import("node:fs/promises");
+ await mkdir(directory);
+ return true;
+ }
return actual.createWindowsPrivateMigrationDirectory(...args);
},
copyWindowsConfigSecurityDescriptors: async (
...args: Parameters
): Promise => {
if (migrationRace.timeoutDiagnosticActive) migrationRace.timeoutDiagnosticPhase = "security-descriptor";
+ // The dedicated Windows migration ACL suite exercises the real trusted
+ // helper end to end. This suite owns filesystem transaction semantics.
+ if (process.platform === "win32") return true;
return actual.copyWindowsConfigSecurityDescriptors(...args);
}
};
diff --git a/tests/console-server.test.ts b/tests/console-server.test.ts
index d9cac280..c7c304f9 100644
--- a/tests/console-server.test.ts
+++ b/tests/console-server.test.ts
@@ -1214,6 +1214,11 @@ async function clearProfileReadinessStateWhenConfigurationIsUnselected(javascrip
focus(): void {}
select(): void {}
+
+ setAttribute(name: string, value: string): void {
+ void name;
+ void value;
+ }
}
class FakeForm extends FakeElement {
@@ -1629,6 +1634,38 @@ function observePresetFieldConstraintState(javascript: string): {
}
describe("local Console control server", () => {
+ it("puts connections and named accounts before collapsed authentication reference", async () => {
+ const server = await startConsoleServer(await writeConfig(), {
+ bootstrapCredential: "test-only-bootstrap-credential"
+ });
+
+ try {
+ const page = await fetch(server.url);
+ expect(page.status).toBe(200);
+ const html = await page.text();
+ const connectionCatalog = html.indexOf('id="configuration-catalog-view"');
+ const setupWizard = html.indexOf('id="setup-wizard-view"');
+ const authenticationReference = html.indexOf('id="authentication-guide"');
+
+ expect(connectionCatalog).toBeGreaterThan(-1);
+ expect(setupWizard).toBeGreaterThan(connectionCatalog);
+ expect(authenticationReference).toBeGreaterThan(setupWizard);
+ expect(html).toContain("How authentication works");
+ expect(html).toContain("One connection, named accounts");
+ expect(html).toContain("Default for new connections");
+ expect(html).toContain("Live account switch");
+
+ const script = await fetch(new URL("/app.js", server.url));
+ expect(script.status).toBe(200);
+ const javascript = await script.text();
+ expect(javascript).toContain("configuration.profileNames");
+ expect(javascript).toContain("configuration.profileSwitchingFromMcp");
+ expect(javascript).toContain("miftah_use_profile");
+ } finally {
+ await server.close();
+ }
+ });
+
it("serves a navigation-safe local dashboard shell without exposing bootstrap credentials", async () => {
const server = await startConsoleServer(await writeConfig(), {
bootstrapCredential: "test-only-bootstrap-credential"
@@ -3123,7 +3160,8 @@ describe("local Console control server", () => {
name: "gsc",
defaultProfile: "work",
upstream: { transport: "stdio", command: "uvx", args: ["mcp-search-console@0.3.2"] },
- profiles: { work: {} }
+ profiles: { work: {}, personal: {} },
+ security: { allowProfileSwitchingFromMcp: true }
})}\n`);
const server = await startConsoleServer(join(directory, "miftah.json"), {
@@ -3144,10 +3182,25 @@ describe("local Console control server", () => {
});
expect(initial.status).toBe(200);
const initialBody = await initial.json() as {
- data: { initialized: boolean; catalog?: { configurations: Array<{ id: string; name: string }> } };
+ data: {
+ initialized: boolean;
+ catalog?: {
+ configurations: Array<{
+ id: string;
+ name: string;
+ profileNames: string[];
+ profileSwitchingFromMcp: boolean;
+ }>;
+ };
+ };
};
expect(initialBody.data.initialized).toBe(false);
expect(initialBody.data.catalog?.configurations).toHaveLength(1);
+ expect(initialBody.data.catalog?.configurations[0]).toMatchObject({
+ name: "gsc",
+ profileNames: ["personal", "work"],
+ profileSwitchingFromMcp: true
+ });
expect(JSON.stringify(initialBody)).not.toContain(directory);
const id = initialBody.data.catalog?.configurations[0]?.id;
if (id === undefined) throw new Error("Expected a discovered configuration id.");
diff --git a/tests/release-version.test.ts b/tests/release-version.test.ts
index 135369af..0a354de2 100644
--- a/tests/release-version.test.ts
+++ b/tests/release-version.test.ts
@@ -1,7 +1,7 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
-const releaseVersion = "0.5.4";
+const releaseVersion = "0.5.5";
function readRepositoryFile(path: string): string {
return readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
@@ -21,15 +21,15 @@ function releaseNotes(changelog: string, version: string): string {
return changelog.slice(match.index, end < 0 ? undefined : end);
}
-describe("v0.5.4 release artifacts", () => {
+describe("v0.5.5 release artifacts", () => {
it.each([
{
name: "a non-zero-padded date",
- changelog: "## [0.5.4] - 2026-7-30\n\n### Changed\n"
+ changelog: "## [0.5.5] - 2026-7-31\n\n### Changed\n"
},
{
name: "a heading that does not start its line",
- changelog: "Release candidate: ## [0.5.4] - 2026-07-30\n\n### Changed\n"
+ changelog: "Release candidate: ## [0.5.5] - 2026-07-31\n\n### Changed\n"
}
])("rejects $name", ({ changelog }) => {
expect(() => releaseNotes(changelog, releaseVersion)).toThrow(
@@ -71,7 +71,7 @@ describe("v0.5.4 release artifacts", () => {
}
});
- it("documents returning-user setup while retaining the experimental package status", () => {
+ it("documents the task-first Console while retaining the experimental package status", () => {
const changelog = readRepositoryFile("CHANGELOG.md");
const notes = releaseNotes(changelog, releaseVersion);
@@ -80,13 +80,15 @@ describe("v0.5.4 release artifacts", () => {
const changedStart = notes.indexOf("### Changed");
const changedEnd = notes.indexOf("\n### ", changedStart + "### Changed".length);
const changedNotes = notes.slice(changedStart, changedEnd < 0 ? undefined : changedEnd);
- for (const issue of [204, 314]) {
+ for (const issue of [202, 319]) {
expect(changedNotes).toContain(`[#${issue}](https://github.com/mohanagy/miftah/issues/${issue})`);
}
- expect(notes).toMatch(/one setup path at a time/iu);
- expect(notes).toMatch(/Back and Cancel provide no-write recovery/iu);
- expect(notes).toMatch(/without creating or changing a configuration/iu);
- expect(notes).toMatch(/external validation remains incomplete/iu);
+ expect(notes).toMatch(/task-first/iu);
+ expect(notes).toMatch(
+ /named account profiles, durable default, and whether live in-session switching through `miftah_use_profile` is available/iu
+ );
+ expect(notes).toContain("collapsed behind **How authentication works**");
+ expect(notes).toContain("external validation remains incomplete under #25, #88, and #202");
const readme = readRepositoryFile("README.md");
const featureGuide = readRepositoryFile("docs/whats-new-in-0.5.md");
diff --git a/tests/tooling-config.test.ts b/tests/tooling-config.test.ts
index fb3dec93..a165d4bc 100644
--- a/tests/tooling-config.test.ts
+++ b/tests/tooling-config.test.ts
@@ -43,4 +43,15 @@ describe("repository tooling contracts", () => {
expect(windowsVerifier.slice(0, functionIndex)).toMatch(jsdocAdjacencyPattern);
});
+
+ it("serializes complete core test files on Windows so ACL helpers cannot exhaust one another", () => {
+ const workflow = readRepositoryFile(".github/workflows/ci.yml");
+
+ expect(workflow).toMatch(
+ /- name: Test core contracts \(Windows, serialized files\)\s+if: \$\{\{ runner\.os == 'Windows' \}\}\s+run: npm run test:core -- --no-file-parallelism[ \t]*(?:\r?\n|$)/u
+ );
+ expect(workflow).toMatch(
+ /- name: Test core contracts\s+if: \$\{\{ runner\.os != 'Windows' \}\}\s+run: npm run test:core[ \t]*(?:\r?\n|$)/u
+ );
+ });
});