gmail proxy tool with token refresh, claude pro bearer auth, pi --list-models stderr fallback, tool object input type

- add getValidGoogleAccessToken helper and use it in email-cron, email account auth resolver, and the new gmail proxy
- POST /api/integrations/google/gmail-proxy forwards arbitrary gmail rest calls server-side, with auto-refreshed oauth
- pi-manager and claude user-instance inject OFFICER_API_URL + per-session JWT so tools can call back as the user
- claude anthropic proxy uses Authorization: Bearer + preserves any anthropic-beta headers (pro oauth tokens are rejected via x-api-key, and overwriting the beta header broke context_management)
- pi --list-models: fall back to stderr when stdout is empty (pi v0.73.1 writes the table to stderr)
- mcp tool server + pi tool loader: accept type: object inputs so json bodies stay structured

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:07:51 +00:00
co-authored by Claude Opus 4.7
parent 8a583da19b
commit 3540d53a00
10 changed files with 129 additions and 17 deletions
@@ -10,6 +10,7 @@ import {
getDockPaths,
setDockPaths,
} from 'officerdb';
import { getValidGoogleAccessToken } from './google-auth';
const GOOGLE_SCOPES = [
'https://mail.google.com/',
@@ -166,6 +167,41 @@ integrationsRouter.delete('/google/connection', async (ctx) => {
return ctx.json({ ok: true });
});
// --- Gmail proxy: forwards arbitrary Gmail REST calls with auto-refreshed OAuth ---
type GmailProxyBody = { method?: string; path?: string; body?: unknown };
const GMAIL_ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
integrationsRouter.post('/google/gmail-proxy', async (ctx) => {
const user = ctx.get('user');
const body = ctx.get('body') as GmailProxyBody;
const method = (body.method ?? '').toUpperCase();
const path = body.path ?? '';
if (!GMAIL_ALLOWED_METHODS.has(method)) throw BAD_REQUEST('Invalid method');
if (!path.startsWith('/')) throw BAD_REQUEST('path must start with /');
const accessToken = await getValidGoogleAccessToken(user.id);
if (!accessToken) throw BAD_REQUEST('No Google account connected — connect in Settings → Integrations');
const url = `https://gmail.googleapis.com/gmail/v1${path}`;
const init: RequestInit = {
method,
headers: { Authorization: `Bearer ${accessToken}` },
};
if (body.body !== undefined && method !== 'GET' && method !== 'DELETE') {
(init.headers as Record<string, string>)['Content-Type'] = 'application/json';
init.body = JSON.stringify(body.body);
}
const upstream = await fetch(url, init);
const text = await upstream.text();
let parsed: unknown;
try { parsed = JSON.parse(text); } catch { parsed = text; }
return ctx.json({ status: upstream.status, ok: upstream.ok, body: parsed });
});
// --- OAuth flow: authorize (protected — user must be logged in) ---
integrationsRouter.get('/google/authorize', async (ctx) => {