Skip to content
Open
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
28 changes: 13 additions & 15 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,16 @@ supply their own boundary. See
1. **OS user account.** Tools run with the user's privileges. The
user is expected to run Maka as a non-admin account on systems
where that matters.
2. **Credential-at-rest boundaries.** The provider credential store
writes `credentials.json` as versioned plaintext JSON under the
user's workspace directory. Its load-bearing boundary is the OS
user account plus filesystem controls: directory mode 0o700,
file mode 0o600, atomic writes, and no symlink/traversal escape.
Subscription OAuth tokens (Claude, Codex, GitHub Copilot, and xAI) live in
the same store: `credentials.json`
is the single authority every Runtime Host surface — Desktop, TUI, CLI —
reads and writes, under the same OS-account and 0o700/0o600 boundary
as other runtime credentials. Electron safeStorage is not part of
this boundary anymore. Pre-existing safeStorage-encrypted credential
or token files are not imported; users with only those copies must
re-authenticate.
2. **Credential-at-rest boundaries.** Runtime Policy writes
`credential-vault.json` as versioned plaintext JSON under the Runtime Host
State Root. Its load-bearing boundary is the OS user account plus filesystem
controls: directory mode 0o700, file mode 0o600, atomic writes, and no
symlink/traversal escape. Subscription OAuth tokens (Claude, Codex, GitHub
Copilot, and xAI) and Amazon Bedrock IAM Identity Center sessions live in
that same vault, which is the single authority every Runtime Host surface —
Desktop, TUI, CLI — uses. Electron safeStorage is not part of this boundary
anymore. Pre-existing safeStorage-encrypted credential or token files are
not imported; users with only those copies must re-authenticate.
3. **Renderer process sandbox + preload IPC bridge.** The
renderer cannot reach files, network, or shell directly. Every
IPC handler in `apps/desktop/src/main/main.ts` is the trust
Expand Down Expand Up @@ -161,8 +158,9 @@ privacy commitments:
reports `incognitoActive: true`, the WebSearch tool fails
closed before any network call. Main composition and focused
consumer tests own the full enforcement inventory.
- **Token boundary.** Cleartext API keys / OAuth tokens / bot
tokens NEVER cross the main→renderer IPC boundary.
- **Token boundary.** Cleartext API keys / OAuth tokens / AWS SSO sessions /
bot tokens NEVER cross the main→renderer IPC boundary. Temporary Bedrock
role credentials are memory-only inside Runtime Host.
`apps/desktop/src/main/__tests__/web-search-boundary.test.ts` and
`claude-subscription-ipc-boundary.test.ts` enforce this for Tavily
and Claude subscription credentials.
Expand Down
8,341 changes: 7,472 additions & 869 deletions apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions apps/desktop/src/main/runtime-host-bedrock-sso-ipc-main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { randomUUID } from 'node:crypto';
import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';
import type { RuntimeHostOAuthPresentation } from './runtime-host-oauth-presentation.js';

export const BEDROCK_SSO_IPC_CHANNELS = [
'amazon-bedrock-sso:get-state',
'amazon-bedrock-sso:start',
'amazon-bedrock-sso:query',
'amazon-bedrock-sso:cancel',
'amazon-bedrock-sso:list-accounts',
'amazon-bedrock-sso:list-roles',
'amazon-bedrock-sso:fetch-models',
'amazon-bedrock-sso:commit',
] as const;

export function registerRuntimeHostBedrockSsoIpc(input: {
readonly ipcMain: ReconnectableReadIpcMain;
readonly client: DesktopRuntimeHostClient;
readonly presentation: RuntimeHostOAuthPresentation;
readonly emitConnectionListChanged: () => void;
}): void {
input.ipcMain.handle('amazon-bedrock-sso:get-state', async () => {
const catalog = await input.client.loadConnectionCatalog();
const connection = catalog.connections.find((candidate) => candidate.providerType === 'amazon-bedrock');
if (!connection) return { runtimeState: 'not_logged_in' as const };
const credential = await input.client.queryCredential({
scope: 'connection',
connectionId: connection.connectionId,
kind: 'aws_sso',
});
return credential?.configured
? {
runtimeState: 'authenticated' as const,
accountId: connection.bedrock?.accountId,
roleName: connection.bedrock?.roleName,
region: connection.bedrock?.region,
}
: { runtimeState: 'not_logged_in' as const };
});
input.ipcMain.handle('amazon-bedrock-sso:start', async (_event, configuration: unknown) => {
if (!isStartConfiguration(configuration)) throw new Error('Invalid Amazon Bedrock SSO configuration');
const attemptId = randomUUID();
const expectation = input.presentation.expect(attemptId);
try {
const projection = await input.client.startBedrockSsoLogin({ attemptId, ...configuration });
await expectation.presented;
return projection;
} catch (error) {
expectation.cancel(error);
await input.client.cancelBedrockSsoLogin(attemptId).catch(() => undefined);
throw error;
}
});
input.ipcMain.handle('amazon-bedrock-sso:query', (_event, attemptId: unknown) =>
input.client.queryBedrockSsoLogin(requireString(attemptId)),
);
input.ipcMain.handle('amazon-bedrock-sso:cancel', (_event, attemptId: unknown) => {
const id = requireString(attemptId);
input.presentation.cancel(id);
return input.client.cancelBedrockSsoLogin(id);
});
input.ipcMain.handle('amazon-bedrock-sso:list-accounts', (_event, attemptId: unknown) =>
input.client.listBedrockSsoAccounts(requireString(attemptId)),
);
input.ipcMain.handle(
'amazon-bedrock-sso:list-roles',
(_event, attemptId: unknown, accountId: unknown) =>
input.client.listBedrockSsoRoles(requireString(attemptId), requireString(accountId)),
);
input.ipcMain.handle(
'amazon-bedrock-sso:fetch-models',
(
_event,
attemptId: unknown,
accountId: unknown,
roleName: unknown,
manualModelIds: unknown,
) =>
input.client.fetchBedrockSsoModels({
attemptId: requireString(attemptId),
accountId: requireString(accountId),
roleName: requireString(roleName),
manualModelIds: requireStrings(manualModelIds),
}),
);
input.ipcMain.handle(
'amazon-bedrock-sso:commit',
async (_event, attemptId: unknown, enabledModelIds: unknown) => {
const result = await input.client.commitBedrockSsoOnboarding({
attemptId: requireString(attemptId),
enabledModelIds: requireStrings(enabledModelIds),
});
input.emitConnectionListChanged();
return result;
},
);
}

function isStartConfiguration(value: unknown): value is {
ssoStartUrl: string;
ssoRegion: string;
region: string;
} {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const record = value as Record<string, unknown>;
return (
Object.keys(record).length === 3 &&
typeof record.ssoStartUrl === 'string' &&
typeof record.ssoRegion === 'string' &&
typeof record.region === 'string'
);
}

function requireString(value: unknown): string {
if (typeof value !== 'string' || value.length === 0) throw new Error('Invalid Bedrock SSO input');
return value;
}

function requireStrings(value: unknown): string[] {
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
throw new Error('Invalid Bedrock SSO list input');
}
return value;
}
21 changes: 19 additions & 2 deletions apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import {
} from "./runtime-host-profile-service.js";
import { createDesktopRuntimeHostSshTerminal } from "./runtime-host-ssh-terminal.js";
import { registerRuntimeHostOAuthIpc } from "./runtime-host-oauth-ipc-main.js";
import { registerRuntimeHostBedrockSsoIpc } from "./runtime-host-bedrock-sso-ipc-main.js";
import { RuntimeHostOAuthPresentation } from "./runtime-host-oauth-presentation.js";
import { registerRuntimeHostPermissionsIpc } from "./runtime-host-permissions-ipc-main.js";
import { registerRuntimeHostRendererIpc } from "./runtime-host-renderer-ipc-main.js";
Expand Down Expand Up @@ -863,6 +864,12 @@ function registerHostClientIpc(
presentation: oauthPresentation,
emitConnectionListChanged: emitTargetConnectionListChanged,
});
registerRuntimeHostBedrockSsoIpc({
ipcMain: scopedIpc,
client,
presentation: oauthPresentation,
emitConnectionListChanged: emitTargetConnectionListChanged,
});
registerRuntimeHostGitHubCopilotIpc({
ipcMain: scopedIpc,
client,
Expand Down Expand Up @@ -1016,7 +1023,12 @@ function registerHostClientIpc(
const status = await client.queryCredential({
scope: "connection",
connectionId: entry.connectionId,
kind: authKind === "oauth_token" ? "oauth_token" : "api_key",
kind:
authKind === "oauth_token"
? "oauth_token"
: authKind === "aws_sso"
? "aws_sso"
: "api_key",
});
return status?.configured === true;
}, false),
Expand Down Expand Up @@ -1048,7 +1060,12 @@ function registerHostClientIpc(
const hasSecret = await client.queryCredential({
scope: "connection",
connectionId: entry.connectionId,
kind: authKind === "oauth_token" ? "oauth_token" : "api_key",
kind:
authKind === "oauth_token"
? "oauth_token"
: authKind === "aws_sso"
? "aws_sso"
: "api_key",
}).then((status) => status?.configured === true);
return { kind: "resolved", connection, hasSecret } as const;
}, { kind: "unknown" }),
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,49 @@ export class DesktopRuntimeHostClient {
return this.request("oauth.account.usage.fetch", { connectionId });
}

startBedrockSsoLogin(
input: OperationInput<"bedrock.sso.login.start">,
): Promise<OperationOutput<"bedrock.sso.login.start">> {
return this.request("bedrock.sso.login.start", input);
}

queryBedrockSsoLogin(
attemptId: string,
): Promise<OperationOutput<"bedrock.sso.login.query">> {
return this.request("bedrock.sso.login.query", { attemptId });
}

cancelBedrockSsoLogin(
attemptId: string,
): Promise<OperationOutput<"bedrock.sso.login.cancel">> {
return this.request("bedrock.sso.login.cancel", { attemptId });
}

listBedrockSsoAccounts(
attemptId: string,
): Promise<OperationOutput<"bedrock.sso.accounts.list">> {
return this.request("bedrock.sso.accounts.list", { attemptId });
}

listBedrockSsoRoles(
attemptId: string,
accountId: string,
): Promise<OperationOutput<"bedrock.sso.roles.list">> {
return this.request("bedrock.sso.roles.list", { attemptId, accountId });
}

fetchBedrockSsoModels(
input: OperationInput<"bedrock.sso.models.fetch">,
): Promise<OperationOutput<"bedrock.sso.models.fetch">> {
return this.request("bedrock.sso.models.fetch", input);
}

commitBedrockSsoOnboarding(
input: OperationInput<"bedrock.sso.onboarding.commit">,
): Promise<OperationOutput<"bedrock.sso.onboarding.commit">> {
return this.request("bedrock.sso.onboarding.commit", input);
}

async loadSkillCatalog(
context: SkillCatalogWorkspaceContext,
view: SkillCatalogView,
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/runtime-host-config-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ function connectionCredentialLocator(
connection: ConnectionCatalogEntry,
): Extract<CredentialLocator, { scope: 'connection' }> | null {
const kind = PROVIDER_DEFAULTS[connection.providerType].authKind;
if (kind === 'none') return null;
// IAM Identity Center sessions and OIDC client secrets are never exported
// through Desktop configuration bundles. Reauthorization is required.
if (kind === 'none' || kind === 'aws_sso') return null;
return {
scope: 'connection',
connectionId: connection.connectionId,
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/runtime-host-connections-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@ function connectionCredential(connection: ConnectionCatalogEntry): CredentialLoc
return {
scope: 'connection',
connectionId: connection.connectionId,
kind: authKind === 'oauth_token' ? 'oauth_token' : 'api_key',
kind:
authKind === 'oauth_token' ? 'oauth_token' : authKind === 'aws_sso' ? 'aws_sso' : 'api_key',
};
}

Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,30 @@ export interface MakaBridge {
refreshTokens(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
logout(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
};
amazonBedrockSso: {
getState(host?: DesktopRuntimeHostRef): Promise<{
runtimeState: 'not_logged_in' | 'authenticated';
accountId?: string;
roleName?: string;
region?: string;
}>;
start(
configuration: { ssoStartUrl: string; ssoRegion: string; region: string },
host?: DesktopRuntimeHostRef,
): Promise<OperationOutput<'bedrock.sso.login.start'>>;
query(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.login.query'>>;
cancel(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.login.cancel'>>;
listAccounts(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.accounts.list'>>;
listRoles(attemptId: string, accountId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.roles.list'>>;
fetchModels(
attemptId: string,
accountId: string,
roleName: string,
manualModelIds: string[],
host?: DesktopRuntimeHostRef,
): Promise<OperationOutput<'bedrock.sso.models.fetch'>>;
commit(attemptId: string, enabledModelIds: string[], host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.onboarding.commit'>>;
};
openAiCodex: {
isExperimentalEnabled(host?: DesktopRuntimeHostRef): Promise<boolean>;
getAuthUrl(host?: DesktopRuntimeHostRef): Promise<AuthorizationUrlPayload | SubscriptionActionResult>;
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,46 @@ const makaBridge = {
return invokeSelectedRuntimeHost(host, 'claude-subscription:logout');
},
},
amazonBedrockSso: {
getState(host?: DesktopRuntimeHostRef): Promise<{
runtimeState: 'not_logged_in' | 'authenticated';
accountId?: string;
roleName?: string;
region?: string;
}> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:get-state');
},
start(
configuration: { ssoStartUrl: string; ssoRegion: string; region: string },
host?: DesktopRuntimeHostRef,
): Promise<OperationOutput<'bedrock.sso.login.start'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:start', configuration);
},
query(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.login.query'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:query', attemptId);
},
cancel(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.login.cancel'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:cancel', attemptId);
},
listAccounts(attemptId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.accounts.list'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:list-accounts', attemptId);
},
listRoles(attemptId: string, accountId: string, host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.roles.list'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:list-roles', attemptId, accountId);
},
fetchModels(
attemptId: string,
accountId: string,
roleName: string,
manualModelIds: string[],
host?: DesktopRuntimeHostRef,
): Promise<OperationOutput<'bedrock.sso.models.fetch'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:fetch-models', attemptId, accountId, roleName, manualModelIds);
},
commit(attemptId: string, enabledModelIds: string[], host?: DesktopRuntimeHostRef): Promise<OperationOutput<'bedrock.sso.onboarding.commit'>> {
return invokeSelectedRuntimeHost(host, 'amazon-bedrock-sso:commit', attemptId, enabledModelIds);
},
},
// Browser-assisted Codex account bridge. Same shape as
// `claudeSubscription`: no token-shaped fields cross preload, the
// authorization attempt stays opaque, and actions return envelopes.
Expand Down
Loading