refactor(v2): remove trigger_credential_collection MCP tool

Drops the in-chat credential-collection flow introduced in e92b245. Agents
can no longer collect API keys via a secure modal — users must add secrets
through OneCLI directly. Keeps the OneCLI manual-approval handler and
threaded-routing work from the same commit intact.

Removed:
* container/agent-runner/src/mcp-tools/credentials.ts (MCP tool)
* src/credentials.ts (host-side modal/OneCLI pipeline)
* src/db/credentials.ts + migration 005 (pending_credentials table)
* src/onecli-secrets.ts (createSecret CLI facade, only caller was credentials.ts)
* findCredentialResponse from agent-runner DB layer
* PendingCredential types
* Four credential hooks from ChannelSetup (getCredentialForModal,
  onCredentialReject, onCredentialSubmit, onCredentialChannelUnsupported)
* Credential card/modal handling in chat-sdk-bridge (nccr/nccm prefixes,
  Modal/TextInput imports)
* credential_request text fallback in WhatsApp adapter
* request_credential system-action case in delivery.ts

Added:
* Migration 009 drops pending_credentials on existing installs.

Vercel skill now tells the agent to ask the user to register the token via
OneCLI instead of invoking the removed tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-16 21:41:41 +03:00
parent e55ed0f4e8
commit cc784ff94b
23 changed files with 29 additions and 823 deletions
-1
View File
@@ -14,7 +14,6 @@ export {
markFailed,
getMessageIn,
findQuestionResponse,
findCredentialResponse,
} from './messages-in.js';
export type { MessageInRow } from './messages-in.js';
export { writeMessageOut, getUndeliveredMessages } from './messages-out.js';
@@ -113,19 +113,3 @@ export function findQuestionResponse(questionId: string): MessageInRow | undefin
return response;
}
/** Find a pending credential_response system message for a given credential id. */
export function findCredentialResponse(credentialId: string): MessageInRow | undefined {
const inbound = getInboundDb();
const outbound = getOutboundDb();
const response = inbound
.prepare("SELECT * FROM messages_in WHERE status = 'pending' AND kind = 'system' AND content LIKE ?")
.get(`%"credentialId":"${credentialId}"%`) as MessageInRow | undefined;
if (!response) return undefined;
const acked = outbound.prepare('SELECT 1 FROM processing_ack WHERE message_id = ?').get(response.id);
if (acked) return undefined;
return response;
}
@@ -1,132 +0,0 @@
/**
* Credential collection MCP tool.
*
* trigger_credential_collection sends a card to the user and blocks until the
* host reports back whether the credential was saved, rejected, or failed.
* The credential value NEVER enters agent context — the user submits it into
* a modal whose value is consumed entirely on the host side, and the host
* only writes back a status string.
*/
import { findCredentialResponse, markCompleted } from '../db/messages-in.js';
import { writeMessageOut } from '../db/messages-out.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
console.error(`[mcp-tools] ${msg}`);
}
function generateId(): string {
return `cred-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function ok(text: string) {
return { content: [{ type: 'text' as const, text }] };
}
function err(text: string) {
return { content: [{ type: 'text' as const, text: `Error: ${text}` }], isError: true };
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const triggerCredentialCollection: McpToolDefinition = {
tool: {
name: 'trigger_credential_collection',
description:
'Collect an API key / OAuth token / secret from the user for a third-party service. Research the service first so you pass the correct host pattern, header name, and value format. The value is injected straight into OneCLI and never enters your context. Blocks until saved/rejected/failed.',
inputSchema: {
type: 'object' as const,
properties: {
name: {
type: 'string',
description: 'Display name for the secret (e.g. "Resend API Key").',
},
type: {
type: 'string',
enum: ['generic', 'anthropic'],
description: "Secret type. Use 'generic' for most third-party APIs; 'anthropic' is reserved for Anthropic API keys.",
},
hostPattern: {
type: 'string',
description: 'Host pattern to match (e.g. "api.resend.com"). Used by OneCLI to know when to inject this credential.',
},
pathPattern: {
type: 'string',
description: 'Optional path pattern to match (e.g. "/v1/*").',
},
headerName: {
type: 'string',
description: 'Header name to inject the credential into (e.g. "Authorization"). Required for generic type.',
},
valueFormat: {
type: 'string',
description: 'Value format template. Use {value} as the placeholder. Example: "Bearer {value}". Defaults to "{value}".',
},
description: {
type: 'string',
description: 'User-facing explanation shown on the card and in the input modal.',
},
timeout: {
type: 'number',
description: 'Timeout in seconds (default: 600).',
},
},
required: ['name', 'hostPattern'],
},
},
async handler(args) {
const name = args.name as string;
const type = ((args.type as string) || 'generic') as 'generic' | 'anthropic';
const hostPattern = args.hostPattern as string;
const pathPattern = (args.pathPattern as string) || '';
const headerName = (args.headerName as string) || '';
const valueFormat = (args.valueFormat as string) || '';
const description = (args.description as string) || '';
const timeoutMs = ((args.timeout as number) || 600) * 1000;
if (!name || !hostPattern) return err('name and hostPattern are required');
const credentialId = generateId();
writeMessageOut({
id: credentialId,
kind: 'system',
content: JSON.stringify({
action: 'request_credential',
credentialId,
name,
type,
hostPattern,
pathPattern,
headerName,
valueFormat,
description,
}),
});
log(`trigger_credential_collection: ${credentialId}${name} (${hostPattern})`);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const response = findCredentialResponse(credentialId);
if (response) {
const parsed = JSON.parse(response.content) as {
status: 'saved' | 'rejected' | 'failed';
detail?: string;
};
markCompleted([response.id]);
log(`trigger_credential_collection result: ${credentialId}${parsed.status}`);
if (parsed.status === 'saved') return ok(parsed.detail || 'Credential saved.');
if (parsed.status === 'rejected') return err(parsed.detail || 'Credential request rejected.');
return err(parsed.detail || 'Credential request failed.');
}
await sleep(1000);
}
log(`trigger_credential_collection timeout: ${credentialId}`);
return err(`Credential request timed out after ${timeoutMs / 1000}s`);
},
};
export const credentialTools: McpToolDefinition[] = [triggerCredentialCollection];
@@ -15,7 +15,6 @@ import { schedulingTools } from './scheduling.js';
import { interactiveTools } from './interactive.js';
import { agentTools } from './agents.js';
import { selfModTools } from './self-mod.js';
import { credentialTools } from './credentials.js';
function log(msg: string): void {
console.error(`[mcp-tools] ${msg}`);
@@ -27,7 +26,6 @@ const allTools: McpToolDefinition[] = [
...interactiveTools,
...agentTools,
...selfModTools,
...credentialTools,
];
const toolMap = new Map<string, McpToolDefinition>();
+2 -14
View File
@@ -19,19 +19,7 @@ Before any Vercel operation, verify auth:
vercel whoami --token placeholder
```
If this fails with an auth error, collect the credential:
```
trigger_credential_collection(
name: "Vercel API Token",
hostPattern: "api.vercel.com",
headerName: "Authorization",
valueFormat: "Bearer {value}",
description: "Vercel personal access token. Create one at https://vercel.com/account/tokens"
)
```
Then retry `vercel whoami`.
If this fails with an auth error, ask the user to add a Vercel token to OneCLI. They can create one at https://vercel.com/account/tokens and register it via `onecli secrets create` on the host. Once added, retry `vercel whoami`.
## Deploying
@@ -98,7 +86,7 @@ echo "value" | vercel env add VAR_NAME production --token placeholder
| `Error: Rate limited` | Wait and retry. Don't loop — report to user |
| `Error: You have reached your project limit` | User needs to upgrade Vercel plan or delete unused projects |
| `ENOTFOUND api.vercel.com` | Network issue. Check proxy connectivity |
| Auth error after `vercel whoami` | Credential may be expired. Re-run `trigger_credential_collection` |
| Auth error after `vercel whoami` | Credential may be expired. Ask the user to refresh the Vercel token in OneCLI |
## Building Websites — Delegate to Frontend Engineer