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
8 changes: 8 additions & 0 deletions .changeset/considerable-bronze-woodpecker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@inkeep/agents-core": minor
"@inkeep/agents-api": minor
"@inkeep/agents-manage-ui": minor
"@inkeep/agents-work-apps": minor
---

Upgrade Better Auth to 1.7.16 for the 2026-11-03 security release; this requires zod `^4.6.7` and the runtime database migration, which adds the new OAuth provider tables and columns, rekeys Microsoft accounts to the Entra `oid` claim, and removes sign-in links left behind by deleted SSO providers (GHSA-prpr-5gj3-qqhg). Before upgrading, set each SSO provider's `issuer` column and `oidc_config.issuer` in the database to exactly the `issuer` in its discovery document, since sign-in now verifies it and issuer edits are refused once users have signed in, and note that a first SSO sign-in no longer links to an existing account with the same email.
6 changes: 6 additions & 0 deletions .changeset/positive-peach-toucan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@inkeep/agents-sdk": patch
"@inkeep/agents-cli": patch
---

Require zod ^4.6.7, the version Better Auth 1.7.16 needs, so Inkeep packages share one zod. Schemas that relied on zod 4.3 treating `z.unknown()` and `z.any()` object keys as optional now mark those keys `.optional()` explicitly, so validation behavior is unchanged.
6 changes: 3 additions & 3 deletions agents-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,19 @@
"ai": "6.0.191",
"ajv": "^8.18.0",
"cron-parser": "^5.5.0",
"drizzle-orm": "^0.44.4",
"drizzle-orm": "^0.45.2",
"fetch-to-node": "^2.1.0",
"hono": "^4.12.7",
"hono-pino": "^0.10.1",
"jmespath": "^0.16.0",
"jose": "^6.1.0",
"jose": "^6.2.15",
"llm-info": "^1.0.69",
"pg": "^8.16.3",
"workflow": "4.6.0"
},
"peerDependencies": {
"@hono/zod-openapi": "^1.1.5",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@hono/vite-dev-server": "^0.20.1",
Expand Down
2 changes: 1 addition & 1 deletion agents-api/src/domains/run/session/AgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,7 @@ ${this.statusUpdateState?.config.prompt?.trim() || ''}`;
} else if (value.type === 'object') {
zodType = z.record(z.string(), z.any());
} else {
zodType = z.any();
zodType = z.any().optional();
}

if (value.description) {
Expand Down
4 changes: 2 additions & 2 deletions agents-api/src/domains/run/types/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const VercelAudioVideoPartSchema = z.object({
export const VercelToolApprovalPartSchema = z.object({
type: z.string().regex(/^tool-/, 'Type must start with "tool-"'),
toolCallId: z.string(),
state: z.any(),
state: z.any().optional(),
approval: z
.object({
id: z.string(),
Expand Down Expand Up @@ -272,7 +272,7 @@ export const MessageIdSchema = z
export const VercelMessageSchema = z.object({
id: MessageIdSchema.optional(),
role: z.enum(['system', 'user', 'assistant', 'function', 'tool']),
content: z.any(),
content: z.any().optional(),
parts: z.array(VercelMessagePartSchema).optional(),
});

Expand Down
2 changes: 1 addition & 1 deletion agents-api/src/utils/oauthJwks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function getOAuthIssuer(): string {
* `/mcp` and when that route forwards it to the manage/run domains.
*
* INVARIANT: this triplet must stay in sync with the authorization server's
* `validAudiences`, which the oauthProvider plugin derives from `config.baseURL`
* `resources`, which the oauthProvider plugin derives from `config.baseURL`
* (see `auth.ts`). Both produce `[base, base + '/', base + '/mcp']`, but from
* independent sources (`INKEEP_AGENTS_API_URL` here vs `config.baseURL` there). A
* deployment MUST point both at the same origin or the AS will mint tokens this
Expand Down
2 changes: 1 addition & 1 deletion agents-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"ts-morph": "^26.0.0",
"tsx": "^4.20.5",
"yaml": "^2.8.2",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@types/degit": "^2.8.6",
Expand Down
4 changes: 2 additions & 2 deletions agents-cli/src/commands/pull-v4/generators/agent-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ const BaseAgentSchema = z.strictObject({
models: z.preprocess((v) => v ?? undefined, SubAgentSchema.shape.models),
stopWhen: z.preprocess((v) => v ?? undefined, SubAgentSchema.shape.stopWhen),
// Unrecognized keys: "name", "description", "content", "metadata", "subAgentSkillId", "subAgentId", "createdAt", "updatedAt"
skills: z.unknown(),
skills: z.unknown().optional(),
// Invalid input
canDelegateTo: z.unknown(),
canDelegateTo: z.unknown().optional(),
})
),
tools: z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const ArtifactComponentSchema = z.strictObject({
description: z.preprocess((v) => v || undefined, MySchema.shape.description),
render: z.preprocess((v) => v ?? undefined, MySchema.shape.render),
// Invalid input
props: z.unknown(),
props: z.unknown().optional(),
});

type ArtifactComponentInput = z.input<typeof ArtifactComponentSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ const BaseContextConfigSchema = z.strictObject({
// TODO check these fields
headers: z.string().optional(),
headersReference: z.string().optional(),
schema: z.unknown(),
schema: z.unknown().optional(),
name: z.string().nullish(),
trigger: z.unknown(),
trigger: z.unknown().optional(),
fetchConfig: z.record(z.string(), z.unknown()).optional(),
defaultValue: z.unknown(),
responseSchema: z.unknown(),
defaultValue: z.unknown().optional(),
responseSchema: z.unknown().optional(),
});

const ContextConfigSchema = BaseContextConfigSchema.transform((data) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const DataComponentSchema = z.strictObject({
...MySchema.shape,
description: z.preprocess((v) => v || undefined, MySchema.shape.description),
// Each property must have a "description" for LLM compatibility
props: z.unknown(),
props: z.unknown().optional(),
});

type DataComponentInput = z.input<typeof DataComponentSchema>;
Expand Down
2 changes: 1 addition & 1 deletion agents-cookbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@inkeep/agents-cli": "workspace:*",
"@inkeep/agents-core": "workspace:*",
"@inkeep/agents-sdk": "workspace:*",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@types/node": "^22.18.12",
Expand Down
10 changes: 7 additions & 3 deletions agents-docs/content/visual-builder/sso-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The **Authentication Methods** card lists every sign-in option available to your
|-------|:--------:|-------------|
| **Provider Name** | ✓ | Display name shown to users on the login page (e.g., `Okta`) |
| **Email Domain** | ✓ | Users whose email matches this domain are automatically routed to this IdP (e.g., `acmecorp.com`) |
| **Issuer URL** | ✓ | Your IdP's OIDC discovery base URL (e.g., `https://your-org.okta.com`). OIDC metadata is auto-detected from this URL. |
| **Issuer URL** | ✓ | Your IdP's issuer, exactly as the `issuer` value in its OIDC discovery document (`<issuer>/.well-known/openid-configuration`), including any trailing slash (e.g., `https://your-org.okta.com`, or `https://your-tenant.us.auth0.com/` for Auth0). OIDC metadata is auto-detected from this URL. |
| **Client ID** | ✓ | The OAuth client ID from your IdP application |
| **Client Secret** | ✓ | The OAuth client secret from your IdP application |
| **Scopes** | | Comma-separated OIDC scopes. Defaults to `openid, email, profile, offline_access` |
Expand Down Expand Up @@ -83,6 +83,10 @@ The **Auto-provision members** toggle controls what happens when a user signs in
If a pending invitation exists for a user, auto-provisioning is skipped and the invitation's role assignment is preserved instead.
</Note>

### Users who already have an account

A first SSO sign-in does not attach to an existing account with the same email, such as one created with Google or a password. That sign-in fails, and the login page explains that the email already has an account with a different sign-in method. The user can keep signing in with that method. Linking an existing account to an SSO provider is not available in **Settings**, so users who will sign in through SSO should use it from their first sign-in.

## Editing an SSO Provider

1. Go to **Settings** → **Authentication Methods**
Expand All @@ -91,7 +95,7 @@ The **Auto-provision members** toggle controls what happens when a user signs in
4. Update any fields — leave **Client ID** or **Client Secret** blank to keep the existing values
5. Click **Save Changes**

You can update the display name, email domain, issuer URL, credentials, scopes, and auto-provision setting at any time.
You can update the display name, email domain, scopes, and auto-provision setting at any time. The issuer URL, client ID, and endpoints can only be changed while no user has signed in through the provider yet. After that, the change is refused. Removing the provider and adding it again does not get around this, because removing a provider unlinks its members (see [Removing an SSO Provider](#removing-an-sso-provider)).

## Removing an SSO Provider

Expand All @@ -101,5 +105,5 @@ You can update the display name, email domain, issuer URL, credentials, scopes,
4. Confirm the removal in the dialog

<Warning>
Removing a provider deletes its configuration permanently. Members who relied solely on this provider to sign in will need to use another authentication method. Ensure at least one other method is enabled before removing.
Removing a provider deletes its configuration and unlinks every member who signed in through it. Adding a provider again, even for the same IdP, does not reconnect them: their accounts already exist, so their next SSO sign-in fails as described in [Users who already have an account](#users-who-already-have-an-account). Members who signed in only through this provider can no longer sign in, and there is no way to reconnect them in **Settings**. Make sure members have another sign-in method before removing a provider.
</Warning>
2 changes: 1 addition & 1 deletion agents-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"remark-source-code": "^0.0.3",
"resend": "^6.9.4",
"tailwind-merge": "^3.2.0",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
Expand Down
2 changes: 1 addition & 1 deletion agents-manage-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"tailwind-merge": "^3.3.1",
"ua-parser-js": "^1.0.41",
"use-stick-to-bottom": "^1.1.1",
"zod": "^4.3.6",
"zod": "^4.6.7",
"zustand": "^5.0.7"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export default function AcceptInvitationPage({
return `${baseURL}/?${params.toString()}`;
}

function getErrorCallbackURL() {
return getFullCallbackURL().replace(
`${window.location.origin}/?`,
`${window.location.origin}/login?`
);
}

// Redirect after successful acceptance
function onSuccess(orgId?: string) {
setSuccess(true);
Expand Down Expand Up @@ -230,11 +237,13 @@ export default function AcceptInvitationPage({
? await authClient.signIn.social({
provider: identifier as 'google' | 'microsoft',
callbackURL: getFullCallbackURL(),
errorCallbackURL: getErrorCallbackURL(),
...(emailFromUrl && { loginHint: emailFromUrl }),
})
: await authClient.signIn.sso({
providerId: identifier,
callbackURL: getFullCallbackURL(),
errorCallbackURL: getErrorCallbackURL(),
...(emailFromUrl && { loginHint: emailFromUrl }),
});
if (result?.error) {
Expand Down
17 changes: 16 additions & 1 deletion agents-manage-ui/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { usePostHog } from '@/contexts/posthog';
import { useRuntimeConfig } from '@/contexts/runtime-config';
import { useAuthSession } from '@/hooks/use-auth';
import { CAPTCHA_ERROR_MESSAGE, getCaptchaErrorMessage } from '@/lib/captcha-errors';
import { getSignInErrorMessage } from '@/lib/sign-in-errors';
import { getSafeReturnUrl, isValidReturnUrl } from '@/lib/utils/auth-redirect';

type LoginState =
Expand Down Expand Up @@ -48,7 +49,7 @@ function LoginForm() {

const urlError = searchParams.get('error');
const urlErrorDescription = searchParams.get('error_description');
const initialError = urlError ? `${urlErrorDescription || urlError}`.replace(/_/g, ' ') : null;
const initialError = getSignInErrorMessage(urlError, urlErrorDescription);
const [error, setError] = useState<string | null>(initialError);

const lastMethod = authClient.getLastUsedLoginMethod();
Expand Down Expand Up @@ -101,6 +102,17 @@ function LoginForm() {
return queryString ? `${baseURL}/?${queryString}` : `${baseURL}/`;
}

function getErrorCallbackURL() {
if (typeof window === 'undefined') return '/login';
if (oauthAuthorizeUrl) return getFullCallbackURL();
const params = new URLSearchParams();
if (invitationId) params.set('invitation', invitationId);
if (returnUrl && isValidReturnUrl(returnUrl) && returnUrl !== '/')
params.set('returnUrl', returnUrl);
const queryString = params.toString();
return `${window.location.origin}/login${queryString ? `?${queryString}` : ''}`;
}

const executeMethodSignIn = async (method: MethodOption) => {
setError(null);
setIsLoading(true);
Expand All @@ -114,6 +126,7 @@ function LoginForm() {
email,
providerId: method.providerId,
callbackURL: getFullCallbackURL(),
errorCallbackURL: getErrorCallbackURL(),
});
if (result?.error) {
setError(result.error.message || 'SSO sign in failed');
Expand All @@ -123,6 +136,7 @@ function LoginForm() {
const result = await authClient.signIn.social({
provider: 'google',
callbackURL: getFullCallbackURL(),
errorCallbackURL: getErrorCallbackURL(),
loginHint: email,
});
if (result?.error) {
Expand All @@ -133,6 +147,7 @@ function LoginForm() {
const result = await authClient.signIn.social({
provider: 'microsoft',
callbackURL: getFullCallbackURL(),
errorCallbackURL: getErrorCallbackURL(),
loginHint: email,
});
if (result?.error) {
Expand Down
26 changes: 26 additions & 0 deletions agents-manage-ui/src/lib/__tests__/sign-in-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { ACCOUNT_NOT_LINKED_MESSAGE, getSignInErrorMessage } from '../sign-in-errors';

describe('getSignInErrorMessage', () => {
it('explains the SSO callback spelling of account not linked', () => {
expect(getSignInErrorMessage('account not linked', null)).toBe(ACCOUNT_NOT_LINKED_MESSAGE);
});

it('explains the social callback spelling of account not linked', () => {
expect(getSignInErrorMessage('account_not_linked', null)).toBe(ACCOUNT_NOT_LINKED_MESSAGE);
});

it('prefers the error description for other errors', () => {
expect(getSignInErrorMessage('invalid_provider', 'token_not_verified')).toBe(
'token not verified'
);
});

it('falls back to the error code for other errors', () => {
expect(getSignInErrorMessage('access_denied', null)).toBe('access denied');
});

it('returns null when there is no error', () => {
expect(getSignInErrorMessage(null, 'ignored')).toBeNull();
});
});
11 changes: 11 additions & 0 deletions agents-manage-ui/src/lib/sign-in-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const ACCOUNT_NOT_LINKED_MESSAGE =
'This email already has an account that uses a different sign-in method. Sign in with the method you used before.';

export function getSignInErrorMessage(
error: string | null | undefined,
description: string | null | undefined
): string | null {
if (!error) return null;
if (error.replace(/ /g, '_') === 'account_not_linked') return ACCOUNT_NOT_LINKED_MESSAGE;
return `${description || error}`.replace(/_/g, ' ');
}
2 changes: 1 addition & 1 deletion create-agents-template/apps/agents-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@hono/zod-openapi": "^1.1.5",
"@inkeep/agents-api": "^0.59.4",
"hono": "^4.11.7",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@hono/vite-dev-server": "^0.23.0",
Expand Down
2 changes: 1 addition & 1 deletion create-agents-template/apps/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"mcp-handler": "^1.0.3",
"next": "^16.4.3",
"typescript": "^6.0.2",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"engines": {
"node": ">=22.18.0"
Expand Down
8 changes: 4 additions & 4 deletions create-agents-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@
"@tailwindcss/vite": "^4.1.14",
"dotenv": "^17.2.3",
"tailwindcss": "^4.1.14",
"zod": "^4.3.6"
"zod": "^4.6.7"
},
"devDependencies": {
"@inkeep/agents-cli": "^0.59.4",
"@types/node": "^22.13.14",
"concurrently": "^9.2.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.4",
"drizzle-orm": "^0.45.2",
"knip": "^5.82.0",
"mprocs": "^0.7.3",
"pg": "^8.16.3",
Expand Down Expand Up @@ -109,8 +109,8 @@
"path-to-regexp@>=8 <8.4.0": "8.4.2",
"picomatch@<2.3.2": ">=2.3.2",
"picomatch@>=4 <4.0.4": ">=4.0.4",
"better-auth": "1.6.11",
"@better-auth/sso": "1.6.11"
"better-auth": "1.7.16",
"@better-auth/sso": "1.7.16"
}
}
}
Loading
Loading