Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d99cd6
feat(mastracode): support extra tools and auth-only storage init (#1)
Kitenite Feb 26, 2026
6fd8e2c
feat(harness): rename images→files and use AI SDK FilePart shape (#2)
saddlepaddle Feb 27, 2026
64f6b27
fix: address PR review feedback and add changesets
Kitenite Feb 27, 2026
4811be2
chore(ci): add daily upstream sync workflow for forks
Kitenite Feb 27, 2026
f12ed13
docs: improve changeset descriptions with migration examples
Kitenite Feb 27, 2026
e3e4351
fix(mastracode): route anthropic api_key away from oauth bearer
Kitenite Feb 28, 2026
0444fb0
fix(mastracode): use auth storage only for anthropic/openai auth
Kitenite Feb 28, 2026
8534e16
Merge pull request #3 from superset-sh/kitenite/jet
Kitenite Feb 28, 2026
37202e8
Merge remote-tracking branch 'upstream/main'
github-actions[bot] Feb 28, 2026
267442f
Merge remote-tracking branch 'upstream/main' into kitenite/jet
Kitenite Feb 28, 2026
2b6f926
Merge remote-tracking branch 'upstream/main'
Kitenite Mar 2, 2026
71938ec
Merge branch 'main' into kitenite/jet
Kitenite Mar 2, 2026
854a476
Merge remote-tracking branch 'origin' into kitenite/jet
Kitenite Mar 2, 2026
4c08006
Merge branch 'kitenite/jet' into codex/merge-jet-into-main
Kitenite Mar 2, 2026
5cf97e0
feat(mastracode): add disabledTools API and extract permission rule u…
Kitenite Mar 2, 2026
dd13f27
refactor(mastracode): keep disabledTools change focused
Kitenite Mar 2, 2026
d717519
Merge pull request #4 from superset-sh/kitenite/dimple
Kitenite Mar 2, 2026
acf05f2
Merge remote-tracking branch 'upstream/main' into codex/sync-origin-m…
Kitenite Mar 3, 2026
efdeda2
Merge branch 'main' into main
roaminro Mar 3, 2026
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
18 changes: 18 additions & 0 deletions .changeset/huge-boxes-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@mastra/core': patch
'mastracode': patch
---

Renamed `images` to `files` in `harness.sendMessage(...)` to align with the AI SDK `FilePart` shape.

**Migration**

Before:
```ts
await harness.sendMessage({ content: "Hi", images: [{ data, mimeType }] });
```

After:
```ts
await harness.sendMessage({ content: "Hi", files: [{ data, mediaType, filename }] });
```
17 changes: 17 additions & 0 deletions .changeset/real-wolves-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'mastracode': minor
'@mastra/core': patch
---

Added pre/post hook wrapping for tool execution via `HookManager` and exported `createAuthStorage` for standalone auth provider initialization.

`@mastra/core` receives a patch bump as a peer dependency of `mastracode`.

**New API: `createAuthStorage`**

```ts
import { createAuthStorage } from 'mastracode';

const authStorage = createAuthStorage();
// authStorage is now wired into Claude Max and OpenAI Codex providers
```
5 changes: 5 additions & 0 deletions .changeset/thin-walls-bet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

Added a daily GitHub Action workflow for forks to sync the default branch from `mastra-ai/mastra`.
This is an infrastructure-only change and does not modify published packages.
Comment on lines +1 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Empty changeset frontmatter is invalid for release notes.

Line 1–Line 2 has no package entries, and Line 4–Line 5 documents an infrastructure-only change. This should not ship as a package changeset.

potential_issue

As per coding guidelines: "Each changeset file contains a YAML frontmatter at the top. It will be one or more package names followed by a colon and the type of change."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/thin-walls-bet.md around lines 1 - 5, The changeset file
.changeset/thin-walls-bet.md has an empty YAML frontmatter (the `---` block)
which is invalid; either remove this changeset file entirely (since it’s an
infrastructure-only workflow change) or replace the empty frontmatter with a
valid package entry and change type (e.g., `package-name: patch`) so it becomes
a proper release changeset; ensure the file no longer contains only
infrastructure notes outside the frontmatter.

52 changes: 52 additions & 0 deletions .github/workflows/sync-upstream-fork.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Sync Fork With Upstream

permissions:
contents: write

on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:

jobs:
sync-upstream:
# Run only on forks. Upstream repository does not need this job.
if: ${{ github.repository != 'mastra-ai/mastra' }}
runs-on: ubuntu-latest
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
UPSTREAM_REPO: mastra-ai/mastra
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Configure Git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Sync default branch from upstream
run: |
set -euo pipefail

if git remote get-url upstream >/dev/null 2>&1; then
git remote set-url upstream "https://github.com/${UPSTREAM_REPO}.git"
else
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
fi

git fetch origin "${DEFAULT_BRANCH}"
git fetch upstream "${DEFAULT_BRANCH}"

git checkout -B "${DEFAULT_BRANCH}" "origin/${DEFAULT_BRANCH}"

UPSTREAM_NEW_COMMITS="$(git rev-list --count "origin/${DEFAULT_BRANCH}..upstream/${DEFAULT_BRANCH}")"
if [ "${UPSTREAM_NEW_COMMITS}" -eq 0 ]; then
echo "No upstream changes to sync."
exit 0
fi

git merge --no-edit "upstream/${DEFAULT_BRANCH}"
git push origin "${DEFAULT_BRANCH}"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ If you are a developer and would like to contribute with code, please open an is

Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)

For fork maintainers: this repository includes a daily GitHub Action at `.github/workflows/sync-upstream-fork.yml` that syncs your fork's default branch from `mastra-ai/mastra`.

## Support

We have an [open community Discord](https://discord.gg/BTYqqHKUrf). Come and say hello and let us know if you have any questions or need any help getting things running.
Expand Down
18 changes: 18 additions & 0 deletions mastracode/src/__tests__/create-auth-storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { afterEach, describe, expect, it } from 'vitest';
import { createAuthStorage } from '../index.js';
import { getAuthStorage as getClaudeAuthStorage, setAuthStorage as setClaudeAuthStorage } from '../providers/claude-max.js';
import { getAuthStorage as getOpenAIAuthStorage, setAuthStorage as setOpenAIAuthStorage } from '../providers/openai-codex.js';

describe('createAuthStorage', () => {
afterEach(() => {
setClaudeAuthStorage(undefined as any);
setOpenAIAuthStorage(undefined as any);
Comment on lines +8 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1. Locate and read the test file ==="
fd 'create-auth-storage\.test\.ts$' --type f

echo
echo "=== 2. Read the full test file to understand context ==="
cat -n mastracode/src/__tests__/create-auth-storage.test.ts

echo
echo "=== 3. Find auth storage setter functions ==="
find mastracode -name "*.ts" -type f | xargs rg -l "export.*setClaudeAuthStorage|export.*setOpenAIAuthStorage" 2>/dev/null || echo "Searching in broader context..."

echo
echo "=== 4. Search for setAuthStorage/setClaudeAuthStorage/setOpenAIAuthStorage signatures ==="
rg -n "setClaudeAuthStorage|setOpenAIAuthStorage" mastracode/src --type ts -A 2 -B 2

echo
echo "=== 5. Check if 'undefined' is ever used as valid input elsewhere ==="
rg -n "setClaudeAuthStorage|setOpenAIAuthStorage" --type ts -A 1 | grep -E "(undefined|null)" || echo "No undefined/null patterns found"

echo
echo "=== 6. Search for reset patterns in auth-related files ==="
fd 'auth' mastracode/src --type f --glob '*.ts' | head -20

Repository: mastra-ai/mastra

Length of output: 3425


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find provider files ==="
fd 'claude-max\.ts|openai-codex\.ts' mastracode/src --type f

echo
echo "=== Read claude-max.ts to find setAuthStorage signature ==="
cat -n mastracode/src/providers/claude-max.ts

echo
echo "=== Read openai-codex.ts to find setAuthStorage signature ==="
cat -n mastracode/src/providers/openai-codex.ts

Repository: mastra-ai/mastra

Length of output: 16716


Update auth storage setters to accept undefined for test cleanup.

Lines 8–9 use undefined as any because the setter functions (from claude-max.js and openai-codex.js) are typed to accept only AuthStorage, not undefined. However, their internal state variables are declared as AuthStorage | null, indicating they support nullable values. Update both setter signatures to (storage: AuthStorage | undefined): void to allow typed cleanup without bypassing strict type checking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/__tests__/create-auth-storage.test.ts` around lines 8 - 9,
Update the setter signatures for setClaudeAuthStorage and setOpenAIAuthStorage
so they accept undefined: change their parameter type to (storage: AuthStorage |
undefined): void and adjust any related type annotations so the functions assign
the passed value directly to the internal nullable state (which is declared
AuthStorage | null); this lets tests call setClaudeAuthStorage(undefined) and
setOpenAIAuthStorage(undefined) without using type assertions.

});

it('wires a shared auth storage instance to provider modules', () => {
const authStorage = createAuthStorage();

expect(getClaudeAuthStorage()).toBe(authStorage);
expect(getOpenAIAuthStorage()).toBe(authStorage);
});
});
32 changes: 32 additions & 0 deletions mastracode/src/agents/__tests__/extra-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,38 @@ describe('createDynamicTools – denied tool filtering', () => {
});
});

describe('createDynamicTools – disabledTools filtering', () => {
it('should omit disabled built-in tools', () => {
const getDynamicTools = createDynamicTools(undefined, undefined, undefined, [
'request_sandbox_access',
'execute_command',
]);

const tools = getDynamicTools({ requestContext: makeRequestContext() });
expect(tools).not.toHaveProperty('request_sandbox_access');
expect(tools).not.toHaveProperty('execute_command');
expect(tools).toHaveProperty('view');
});

it('should omit disabled extraTools', () => {
const myTool = createTool({
id: 'my_tool',
description: 'A custom tool',
inputSchema: z.object({}),
execute: async () => ({ result: 'custom' }),
});

const getDynamicTools = createDynamicTools(
undefined,
{ my_tool: myTool },
undefined,
['my_tool'],
);
const tools = getDynamicTools({ requestContext: makeRequestContext() });
expect(tools).not.toHaveProperty('my_tool');
});
});

describe('buildToolGuidance – denied tool filtering', () => {
it('should omit guidance for denied tools', () => {
const guidance = buildToolGuidance('build', {
Expand Down
93 changes: 72 additions & 21 deletions mastracode/src/agents/__tests__/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ vi.mock('@ai-sdk/anthropic', () => ({
}),
}));

// Mock @ai-sdk/openai
vi.mock('@ai-sdk/openai', () => ({
createOpenAI: vi.fn((_opts: Record<string, unknown>) => {
const openai = ((modelId: string) => ({ __provider: 'openai-direct', modelId })) as unknown as {
responses: (modelId: string) => Record<string, unknown>;
};
openai.responses = (modelId: string) => ({ __provider: 'openai-direct', modelId });
return openai;
}),
}));

// Mock ai SDK's wrapLanguageModel to pass through with a marker
vi.mock('ai', () => ({
wrapLanguageModel: vi.fn(({ model }: { model: Record<string, unknown> }) => ({
Expand All @@ -56,7 +67,8 @@ vi.mock('@mastra/core/llm', () => ({
}));

import { opencodeClaudeMaxProvider } from '../../providers/claude-max.js';
import { resolveModel, getAnthropicApiKey } from '../model.js';
import { openaiCodexProvider } from '../../providers/openai-codex.js';
import { resolveModel, getAnthropicApiKey, getOpenAIApiKey } from '../model.js';

describe('resolveModel', () => {
const originalEnv = { ...process.env };
Expand All @@ -72,18 +84,22 @@ describe('resolveModel', () => {
});

describe('anthropic/* models', () => {
it('prefers Claude Max OAuth when logged in, even if API key is present', () => {
process.env.ANTHROPIC_API_KEY = 'sk-test-key-123';
mockAuthStorageInstance.isLoggedIn.mockImplementation((p: string) => p === 'anthropic');
it('prefers Claude Max OAuth when stored OAuth credential exists', () => {
mockAuthStorageInstance.get.mockReturnValue({
type: 'oauth',
access: 'oauth-access-token',
refresh: 'oauth-refresh-token',
expires: Date.now() + 60_000,
});

resolveModel('anthropic/claude-sonnet-4-20250514');

expect(opencodeClaudeMaxProvider).toHaveBeenCalledWith('claude-sonnet-4-20250514');
});

it('falls back to API key when not logged in via OAuth', () => {
process.env.ANTHROPIC_API_KEY = 'sk-test-key-123';
mockAuthStorageInstance.isLoggedIn.mockReturnValue(false);
it('uses API key when stored credential is api_key, even if isLoggedIn reports true', () => {
mockAuthStorageInstance.isLoggedIn.mockImplementation((p: string) => p === 'anthropic');
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-stored-key-456' });

const result = resolveModel('anthropic/claude-sonnet-4-20250514') as Record<string, unknown>;

Expand All @@ -93,6 +109,16 @@ describe('resolveModel', () => {
expect(opencodeClaudeMaxProvider).not.toHaveBeenCalled();
});

it('does not use env API key when no stored Anthropic credential exists', () => {
process.env.ANTHROPIC_API_KEY = 'sk-test-key-123';
mockAuthStorageInstance.get.mockReturnValue(undefined);

const result = resolveModel('anthropic/claude-sonnet-4-20250514') as Record<string, unknown>;

expect(result.__provider).toBe('claude-max-oauth');
expect(opencodeClaudeMaxProvider).toHaveBeenCalledWith('claude-sonnet-4-20250514');
});

it('uses stored API key credential when not logged in via OAuth', () => {
mockAuthStorageInstance.isLoggedIn.mockReturnValue(false);
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-stored-key-456' });
Expand All @@ -106,7 +132,6 @@ describe('resolveModel', () => {
});

it('falls back to OAuth provider when no auth is configured (to prompt login)', () => {
mockAuthStorageInstance.isLoggedIn.mockReturnValue(false);
mockAuthStorageInstance.get.mockReturnValue(undefined);

resolveModel('anthropic/claude-sonnet-4-20250514');
Expand All @@ -122,14 +147,28 @@ describe('resolveModel', () => {
});

describe('openai/* models', () => {
it('uses codex provider when logged in via OAuth', () => {
mockAuthStorageInstance.isLoggedIn.mockReturnValue(true);
it('uses codex provider when stored OAuth credential exists', () => {
mockAuthStorageInstance.get.mockReturnValue({
type: 'oauth',
access: 'openai-oauth-access-token',
refresh: 'openai-oauth-refresh-token',
expires: Date.now() + 60_000,
});
const result = resolveModel('openai/gpt-4o') as Record<string, unknown>;
expect(result.__provider).toBe('openai-codex');
expect(openaiCodexProvider).toHaveBeenCalled();
});

it('uses model router when not logged in via OAuth', () => {
mockAuthStorageInstance.isLoggedIn.mockReturnValue(false);
it('uses direct OpenAI API key provider when stored API key credential exists', () => {
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-openai-key' });
const result = resolveModel('openai/gpt-4o') as Record<string, unknown>;
expect(result.__provider).toBe('openai-direct');
expect(result.__wrapped).toBe(true);
expect(result.modelId).toBe('gpt-4o');
});

it('uses model router when no OpenAI auth is configured', () => {
mockAuthStorageInstance.get.mockReturnValue(undefined);
const result = resolveModel('openai/gpt-4o') as Record<string, unknown>;
expect(result.__provider).toBe('model-router');
});
Expand All @@ -155,12 +194,7 @@ describe('getAnthropicApiKey', () => {
process.env = { ...originalEnv };
});

it('returns env var when ANTHROPIC_API_KEY is set', () => {
process.env.ANTHROPIC_API_KEY = 'sk-env-key';
expect(getAnthropicApiKey()).toBe('sk-env-key');
});

it('returns stored API key when no env var is set', () => {
it('returns stored API key when set', () => {
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-stored-key' });
expect(getAnthropicApiKey()).toBe('sk-stored-key');
});
Expand All @@ -175,9 +209,26 @@ describe('getAnthropicApiKey', () => {
expect(getAnthropicApiKey()).toBeUndefined();
});

it('prefers env var over stored credential', () => {
it('ignores env var when no stored credential exists', () => {
process.env.ANTHROPIC_API_KEY = 'sk-env-key';
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-stored-key' });
expect(getAnthropicApiKey()).toBe('sk-env-key');
mockAuthStorageInstance.get.mockReturnValue(undefined);
expect(getAnthropicApiKey()).toBeUndefined();
});
});

describe('getOpenAIApiKey', () => {
it('returns stored API key when set', () => {
mockAuthStorageInstance.get.mockReturnValue({ type: 'api_key', key: 'sk-openai-key' });
expect(getOpenAIApiKey()).toBe('sk-openai-key');
});

it('returns undefined when no API key is available', () => {
mockAuthStorageInstance.get.mockReturnValue(undefined);
expect(getOpenAIApiKey()).toBeUndefined();
});

it('returns undefined when stored credential is OAuth type', () => {
mockAuthStorageInstance.get.mockReturnValue({ type: 'oauth', access: 'token', refresh: 'r', expires: 0 });
expect(getOpenAIApiKey()).toBeUndefined();
});
});
Loading
Loading