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:
@@ -27,14 +27,6 @@ export interface ChannelSetup {
|
||||
|
||||
/** Called when a user clicks a button/action in a card (e.g., ask_user_question response). */
|
||||
onAction(questionId: string, selectedOption: string, userId: string): void;
|
||||
|
||||
/** Credential collection hooks — used by chat-sdk-bridge to route the modal flow. */
|
||||
getCredentialForModal?(
|
||||
credentialId: string,
|
||||
): { name: string; description: string | null; hostPattern: string } | null;
|
||||
onCredentialReject?(credentialId: string): void;
|
||||
onCredentialSubmit?(credentialId: string, value: string): void;
|
||||
onCredentialChannelUnsupported?(credentialId: string): void;
|
||||
}
|
||||
|
||||
/** Inbound message from adapter to host. */
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
CardText,
|
||||
Actions,
|
||||
Button,
|
||||
Modal,
|
||||
TextInput,
|
||||
type Adapter,
|
||||
type ConcurrencyStrategy,
|
||||
type Message as ChatMessage,
|
||||
@@ -191,72 +189,8 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
await thread.subscribe();
|
||||
});
|
||||
|
||||
// Handle button clicks (ask_user_question, credential card)
|
||||
// Handle button clicks (ask_user_question)
|
||||
chat.onAction(async (event) => {
|
||||
// Credential card actions: nccr:<credentialId>:<enter|reject>
|
||||
if (event.actionId.startsWith('nccr:')) {
|
||||
const [, credentialId, subAction] = event.actionId.split(':');
|
||||
if (!credentialId || !subAction) return;
|
||||
|
||||
if (subAction === 'reject') {
|
||||
try {
|
||||
await adapter.editMessage(event.threadId, event.messageId, {
|
||||
markdown: `🔑 Credential request\n\n❌ Rejected`,
|
||||
});
|
||||
} catch (err) {
|
||||
log.warn('Failed to update credential card after reject', { err });
|
||||
}
|
||||
setupConfig.onCredentialReject?.(credentialId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subAction === 'enter') {
|
||||
const pending = setupConfig.getCredentialForModal?.(credentialId);
|
||||
if (!pending) {
|
||||
log.warn('Credential card clicked but row not pending', { credentialId });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const modalChildren = [
|
||||
CardText(pending.description ?? `Enter the value for ${pending.name} (host: ${pending.hostPattern}).`),
|
||||
TextInput({
|
||||
id: 'value',
|
||||
label: pending.name,
|
||||
placeholder: 'Paste your credential value',
|
||||
}),
|
||||
];
|
||||
// Modal children include a text element for context; the SDK
|
||||
// accepts TextElement in ModalChild so this is valid.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const modal = Modal({
|
||||
callbackId: `nccm:${credentialId}`,
|
||||
title: 'Enter credential',
|
||||
submitLabel: 'Save',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
children: modalChildren as any,
|
||||
});
|
||||
const result = await event.openModal(modal);
|
||||
if (!result) {
|
||||
log.warn('openModal returned undefined — channel unsupported', { credentialId });
|
||||
setupConfig.onCredentialChannelUnsupported?.(credentialId);
|
||||
try {
|
||||
await adapter.editMessage(event.threadId, event.messageId, {
|
||||
markdown: `🔑 Credential request\n\n⚠️ This channel does not support modals.`,
|
||||
});
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to open credential modal', { credentialId, err });
|
||||
setupConfig.onCredentialChannelUnsupported?.(credentialId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.actionId.startsWith('ncq:')) return;
|
||||
const parts = event.actionId.split(':');
|
||||
if (parts.length < 3) return;
|
||||
@@ -283,18 +217,6 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
setupConfig.onAction(questionId, selectedOption, userId);
|
||||
});
|
||||
|
||||
// Modal submissions for credential collection
|
||||
chat.onModalSubmit(async (event) => {
|
||||
if (!event.callbackId.startsWith('nccm:')) return;
|
||||
const credentialId = event.callbackId.slice('nccm:'.length);
|
||||
const value = event.values?.value ?? '';
|
||||
if (!value) {
|
||||
log.warn('Credential modal submitted with empty value', { credentialId });
|
||||
return;
|
||||
}
|
||||
setupConfig.onCredentialSubmit?.(credentialId, value);
|
||||
});
|
||||
|
||||
await chat.initialize();
|
||||
|
||||
// Start Gateway listener for adapters that support it (e.g., Discord)
|
||||
@@ -394,26 +316,6 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
return result?.id;
|
||||
}
|
||||
|
||||
// Credential request card — buttons open a modal for secure input
|
||||
if (content.type === 'credential_request' && content.credentialId) {
|
||||
const credentialId = content.credentialId as string;
|
||||
const card = Card({
|
||||
title: '🔑 Credential request',
|
||||
children: [
|
||||
CardText(content.question as string),
|
||||
Actions([
|
||||
Button({ id: `nccr:${credentialId}:enter`, label: 'Enter credential', value: 'enter' }),
|
||||
Button({ id: `nccr:${credentialId}:reject`, label: 'Reject', value: 'reject' }),
|
||||
]),
|
||||
],
|
||||
});
|
||||
const result = await adapter.postMessage(tid, {
|
||||
card,
|
||||
fallbackText: `Credential request — open in a channel that supports modals.`,
|
||||
});
|
||||
return result?.id;
|
||||
}
|
||||
|
||||
// Normal message
|
||||
const rawText = (content.markdown as string) || (content.text as string);
|
||||
const text = rawText ? transformText(rawText) : rawText;
|
||||
|
||||
@@ -664,14 +664,6 @@ registerChannelAdapter('whatsapp', {
|
||||
return;
|
||||
}
|
||||
|
||||
// Credential request → text fallback (WhatsApp doesn't support modals)
|
||||
if (content.type === 'credential_request' && content.credentialId) {
|
||||
const question = (content.question as string) || 'A credential has been requested.';
|
||||
const text = `Credential request: ${question}\n\nPlease provide this credential through a secure channel (e.g. Discord or Slack).`;
|
||||
const prefixed = ASSISTANT_HAS_OWN_NUMBER ? text : `${ASSISTANT_NAME}: ${text}`;
|
||||
return sendRawMessage(platformId, prefixed);
|
||||
}
|
||||
|
||||
// Normal message (with optional file attachments)
|
||||
const text = (content.markdown as string) || (content.text as string);
|
||||
const hasFiles = message.files && message.files.length > 0;
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Credential collection flow.
|
||||
*
|
||||
* Agent calls `trigger_credential_collection` — container writes a system
|
||||
* action `request_credential` into outbound.db. This module:
|
||||
*
|
||||
* 1. Delivers an `[Enter credential] [Reject]` card to the admin channel.
|
||||
* 2. On "Enter credential" click, the Chat SDK bridge opens a modal with a
|
||||
* TextInput, captures the user's value in `onModalSubmit`, and calls
|
||||
* `handleCredentialSubmit()` here.
|
||||
* 3. We insert the secret into OneCLI and write a system chat message into
|
||||
* the agent's session DB so the blocking MCP tool call returns.
|
||||
* 4. The credential value never enters any session DB or log line.
|
||||
*/
|
||||
import {
|
||||
createPendingCredential,
|
||||
deletePendingCredential,
|
||||
getPendingCredential as getPendingCredentialRow,
|
||||
updatePendingCredentialMessageId,
|
||||
updatePendingCredentialStatus,
|
||||
} from './db/credentials.js';
|
||||
import { getMessagingGroup } from './db/messaging-groups.js';
|
||||
import type { ChannelDeliveryAdapter } from './delivery.js';
|
||||
import { log } from './log.js';
|
||||
import { createSecret, OneCLISecretError } from './onecli-secrets.js';
|
||||
import { writeSessionMessage } from './session-manager.js';
|
||||
import type { PendingCredential, Session } from './types.js';
|
||||
import { wakeContainer } from './container-runner.js';
|
||||
|
||||
let adapterRef: ChannelDeliveryAdapter | null = null;
|
||||
|
||||
export function setCredentialDeliveryAdapter(adapter: ChannelDeliveryAdapter): void {
|
||||
adapterRef = adapter;
|
||||
}
|
||||
|
||||
/** Handle a `request_credential` system action from a container. */
|
||||
export async function handleCredentialRequest(content: Record<string, unknown>, session: Session): Promise<void> {
|
||||
if (!adapterRef) {
|
||||
notifyAgentCredentialResult(session, content.credentialId as string, 'failed', 'delivery adapter not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialId = (content.credentialId as string) || '';
|
||||
const name = (content.name as string) || '';
|
||||
const type = ((content.type as string) || 'generic') as 'generic' | 'anthropic';
|
||||
const hostPattern = (content.hostPattern as string) || '';
|
||||
const pathPattern = (content.pathPattern as string) || null;
|
||||
const headerName = (content.headerName as string) || null;
|
||||
const valueFormat = (content.valueFormat as string) || null;
|
||||
const description = (content.description as string) || null;
|
||||
|
||||
if (!credentialId || !name || !hostPattern) {
|
||||
notifyAgentCredentialResult(session, credentialId, 'failed', 'name and hostPattern are required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deliver the credential card to the channel where the conversation is
|
||||
// happening — not the admin channel. The user triggered this request by
|
||||
// chatting with the agent, so the response surface is their chat channel.
|
||||
if (!session.messaging_group_id) {
|
||||
notifyAgentCredentialResult(
|
||||
session,
|
||||
credentialId,
|
||||
'failed',
|
||||
'session has no messaging group — cannot deliver credential card',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const mg = getMessagingGroup(session.messaging_group_id);
|
||||
if (!mg) {
|
||||
notifyAgentCredentialResult(session, credentialId, 'failed', 'messaging group not found');
|
||||
return;
|
||||
}
|
||||
|
||||
createPendingCredential({
|
||||
id: credentialId,
|
||||
agent_group_id: session.agent_group_id,
|
||||
session_id: session.id,
|
||||
name,
|
||||
type,
|
||||
host_pattern: hostPattern,
|
||||
path_pattern: pathPattern,
|
||||
header_name: headerName,
|
||||
value_format: valueFormat,
|
||||
description,
|
||||
channel_type: mg.channel_type,
|
||||
platform_id: mg.platform_id,
|
||||
platform_message_id: null,
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const question = buildCardText({
|
||||
name,
|
||||
hostPattern,
|
||||
headerName,
|
||||
valueFormat,
|
||||
description,
|
||||
});
|
||||
|
||||
let platformMessageId: string | undefined;
|
||||
try {
|
||||
platformMessageId = await adapterRef.deliver(
|
||||
mg.channel_type,
|
||||
mg.platform_id,
|
||||
session.thread_id,
|
||||
'chat-sdk',
|
||||
JSON.stringify({
|
||||
type: 'credential_request',
|
||||
credentialId,
|
||||
question,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
log.error('Failed to deliver credential request card', { credentialId, err });
|
||||
updatePendingCredentialStatus(credentialId, 'failed');
|
||||
notifyAgentCredentialResult(session, credentialId, 'failed', 'could not deliver card');
|
||||
return;
|
||||
}
|
||||
|
||||
if (platformMessageId) {
|
||||
updatePendingCredentialMessageId(credentialId, platformMessageId);
|
||||
}
|
||||
|
||||
log.info('Credential request delivered', { credentialId, name, hostPattern });
|
||||
}
|
||||
|
||||
/** Called by chat-sdk-bridge to fetch metadata for building the modal. */
|
||||
export function getCredentialForModal(
|
||||
credentialId: string,
|
||||
): { name: string; description: string | null; hostPattern: string } | null {
|
||||
const row = getPendingCredentialRow(credentialId);
|
||||
if (!row || row.status !== 'pending') return null;
|
||||
return { name: row.name, description: row.description, hostPattern: row.host_pattern };
|
||||
}
|
||||
|
||||
/** Admin clicked "Reject" on the card (or cancelled the modal). */
|
||||
export async function handleCredentialReject(credentialId: string): Promise<void> {
|
||||
const row = getPendingCredentialRow(credentialId);
|
||||
if (!row) return;
|
||||
updatePendingCredentialStatus(credentialId, 'rejected');
|
||||
|
||||
if (row.session_id) {
|
||||
await notifyAgentSessionResult(
|
||||
row.agent_group_id,
|
||||
row.session_id,
|
||||
credentialId,
|
||||
'rejected',
|
||||
`Credential request for ${row.name} was rejected by admin.`,
|
||||
);
|
||||
}
|
||||
|
||||
deletePendingCredential(credentialId);
|
||||
log.info('Credential request rejected', { credentialId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin submitted the modal with a credential value.
|
||||
* The value is held only long enough to call OneCLI and is then dropped.
|
||||
*/
|
||||
export async function handleCredentialSubmit(credentialId: string, value: string): Promise<void> {
|
||||
const row = getPendingCredentialRow(credentialId);
|
||||
if (!row) {
|
||||
log.warn('Credential submit for unknown id', { credentialId });
|
||||
return;
|
||||
}
|
||||
if (row.status !== 'pending') {
|
||||
log.warn('Credential submit for non-pending row', { credentialId, status: row.status });
|
||||
return;
|
||||
}
|
||||
|
||||
updatePendingCredentialStatus(credentialId, 'submitted');
|
||||
|
||||
try {
|
||||
await createSecret({
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
value,
|
||||
hostPattern: row.host_pattern,
|
||||
pathPattern: row.path_pattern ?? undefined,
|
||||
headerName: row.header_name ?? undefined,
|
||||
valueFormat: row.value_format ?? undefined,
|
||||
agentId: row.agent_group_id, // honored once OneCLI SDK adds scoping
|
||||
});
|
||||
} catch (err) {
|
||||
const reason = err instanceof OneCLISecretError ? err.message : String(err);
|
||||
log.error('Failed to create OneCLI secret', { credentialId, reason });
|
||||
updatePendingCredentialStatus(credentialId, 'failed');
|
||||
if (row.session_id) {
|
||||
await notifyAgentSessionResult(
|
||||
row.agent_group_id,
|
||||
row.session_id,
|
||||
credentialId,
|
||||
'failed',
|
||||
`Credential save failed: ${reason}`,
|
||||
);
|
||||
}
|
||||
deletePendingCredential(credentialId);
|
||||
return;
|
||||
}
|
||||
|
||||
updatePendingCredentialStatus(credentialId, 'saved');
|
||||
log.info('Credential saved', { credentialId, name: row.name, hostPattern: row.host_pattern });
|
||||
|
||||
if (row.session_id) {
|
||||
await notifyAgentSessionResult(
|
||||
row.agent_group_id,
|
||||
row.session_id,
|
||||
credentialId,
|
||||
'saved',
|
||||
`Credential "${row.name}" saved (host pattern: ${row.host_pattern}).`,
|
||||
);
|
||||
}
|
||||
|
||||
deletePendingCredential(credentialId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for inbound channels that don't support modals — the bridge calls
|
||||
* this when `event.openModal()` is unavailable or returned undefined.
|
||||
*/
|
||||
export async function handleCredentialChannelUnsupported(credentialId: string): Promise<void> {
|
||||
const row = getPendingCredentialRow(credentialId);
|
||||
if (!row) return;
|
||||
updatePendingCredentialStatus(credentialId, 'failed');
|
||||
if (row.session_id) {
|
||||
await notifyAgentSessionResult(
|
||||
row.agent_group_id,
|
||||
row.session_id,
|
||||
credentialId,
|
||||
'failed',
|
||||
`This channel doesn't support credential collection modals. Use Slack, Discord, Teams, or Google Chat.`,
|
||||
);
|
||||
}
|
||||
deletePendingCredential(credentialId);
|
||||
}
|
||||
|
||||
function notifyAgentCredentialResult(
|
||||
session: Session,
|
||||
credentialId: string,
|
||||
status: 'saved' | 'rejected' | 'failed',
|
||||
detail: string,
|
||||
): void {
|
||||
writeSessionMessage(session.agent_group_id, session.id, {
|
||||
id: `cred-${credentialId}-${Date.now()}`,
|
||||
kind: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
platformId: session.agent_group_id,
|
||||
channelType: 'agent',
|
||||
threadId: null,
|
||||
content: JSON.stringify({
|
||||
type: 'credential_response',
|
||||
credentialId,
|
||||
status,
|
||||
detail,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function notifyAgentSessionResult(
|
||||
agentGroupId: string,
|
||||
sessionId: string,
|
||||
credentialId: string,
|
||||
status: 'saved' | 'rejected' | 'failed',
|
||||
detail: string,
|
||||
): Promise<void> {
|
||||
writeSessionMessage(agentGroupId, sessionId, {
|
||||
id: `cred-${credentialId}-${Date.now()}`,
|
||||
kind: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
platformId: agentGroupId,
|
||||
channelType: 'agent',
|
||||
threadId: null,
|
||||
content: JSON.stringify({
|
||||
type: 'credential_response',
|
||||
credentialId,
|
||||
status,
|
||||
detail,
|
||||
}),
|
||||
});
|
||||
|
||||
const { getSession } = await import('./db/sessions.js');
|
||||
const session = getSession(sessionId);
|
||||
if (session) await wakeContainer(session);
|
||||
}
|
||||
|
||||
function buildCardText(opts: {
|
||||
name: string;
|
||||
hostPattern: string;
|
||||
headerName: string | null;
|
||||
valueFormat: string | null;
|
||||
description: string | null;
|
||||
}): string {
|
||||
const lines = [`🔑 Credential request: ${opts.name}`, '', `Host: \`${opts.hostPattern}\``];
|
||||
if (opts.headerName) lines.push(`Header: \`${opts.headerName}\``);
|
||||
if (opts.valueFormat) lines.push(`Format: \`${opts.valueFormat}\``);
|
||||
if (opts.description) lines.push('', opts.description);
|
||||
lines.push('', 'Click Enter credential to provide the value, or Reject to decline.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { PendingCredential, PendingCredentialStatus } from '../types.js';
|
||||
import { getDb } from './connection.js';
|
||||
|
||||
export function createPendingCredential(c: PendingCredential): void {
|
||||
getDb()
|
||||
.prepare(
|
||||
`INSERT INTO pending_credentials
|
||||
(id, agent_group_id, session_id, name, type, host_pattern, path_pattern,
|
||||
header_name, value_format, description, channel_type, platform_id,
|
||||
platform_message_id, status, created_at)
|
||||
VALUES
|
||||
(@id, @agent_group_id, @session_id, @name, @type, @host_pattern, @path_pattern,
|
||||
@header_name, @value_format, @description, @channel_type, @platform_id,
|
||||
@platform_message_id, @status, @created_at)`,
|
||||
)
|
||||
.run(c);
|
||||
}
|
||||
|
||||
export function getPendingCredential(id: string): PendingCredential | undefined {
|
||||
return getDb().prepare('SELECT * FROM pending_credentials WHERE id = ?').get(id) as PendingCredential | undefined;
|
||||
}
|
||||
|
||||
export function updatePendingCredentialStatus(id: string, status: PendingCredentialStatus): void {
|
||||
getDb().prepare('UPDATE pending_credentials SET status = ? WHERE id = ?').run(status, id);
|
||||
}
|
||||
|
||||
export function updatePendingCredentialMessageId(id: string, platformMessageId: string): void {
|
||||
getDb().prepare('UPDATE pending_credentials SET platform_message_id = ? WHERE id = ?').run(platformMessageId, id);
|
||||
}
|
||||
|
||||
export function deletePendingCredential(id: string): void {
|
||||
getDb().prepare('DELETE FROM pending_credentials WHERE id = ?').run(id);
|
||||
}
|
||||
@@ -58,10 +58,3 @@ export {
|
||||
deletePendingApproval,
|
||||
getPendingApprovalsByAction,
|
||||
} from './sessions.js';
|
||||
export {
|
||||
createPendingCredential,
|
||||
getPendingCredential,
|
||||
updatePendingCredentialStatus,
|
||||
updatePendingCredentialMessageId,
|
||||
deletePendingCredential,
|
||||
} from './credentials.js';
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Migration } from './index.js';
|
||||
|
||||
/**
|
||||
* `pending_credentials` — backs the trigger_credential_collection flow.
|
||||
* One row per in-flight credential request; status transitions
|
||||
* pending → submitted → saved | rejected | failed.
|
||||
*/
|
||||
export const migration005: Migration = {
|
||||
version: 5,
|
||||
name: 'pending-credentials',
|
||||
up(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE pending_credentials (
|
||||
id TEXT PRIMARY KEY,
|
||||
agent_group_id TEXT NOT NULL REFERENCES agent_groups(id),
|
||||
session_id TEXT REFERENCES sessions(id),
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
host_pattern TEXT NOT NULL,
|
||||
path_pattern TEXT,
|
||||
header_name TEXT,
|
||||
value_format TEXT,
|
||||
description TEXT,
|
||||
channel_type TEXT NOT NULL,
|
||||
platform_id TEXT NOT NULL,
|
||||
platform_message_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_pending_credentials_status ON pending_credentials(status);
|
||||
`);
|
||||
},
|
||||
};
|
||||
13
src/db/migrations/009-drop-pending-credentials.ts
Normal file
13
src/db/migrations/009-drop-pending-credentials.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { Migration } from './index.js';
|
||||
|
||||
export const migration009: Migration = {
|
||||
version: 9,
|
||||
name: 'drop-pending-credentials',
|
||||
up: (db: Database.Database) => {
|
||||
db.exec(`
|
||||
DROP INDEX IF EXISTS idx_pending_credentials_status;
|
||||
DROP TABLE IF EXISTS pending_credentials;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -5,9 +5,9 @@ import { migration001 } from './001-initial.js';
|
||||
import { migration002 } from './002-chat-sdk-state.js';
|
||||
import { migration003 } from './003-pending-approvals.js';
|
||||
import { migration004 } from './004-agent-destinations.js';
|
||||
import { migration005 } from './005-pending-credentials.js';
|
||||
import { migration007 } from './007-pending-approvals-title-options.js';
|
||||
import { migration008 } from './008-dropped-messages.js';
|
||||
import { migration009 } from './009-drop-pending-credentials.js';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
@@ -20,9 +20,9 @@ const migrations: Migration[] = [
|
||||
migration002,
|
||||
migration003,
|
||||
migration004,
|
||||
migration005,
|
||||
migration007,
|
||||
migration008,
|
||||
migration009,
|
||||
];
|
||||
|
||||
export function runMigrations(db: Database.Database): void {
|
||||
|
||||
@@ -169,9 +169,9 @@ CREATE TABLE IF NOT EXISTS destinations (
|
||||
|
||||
-- Default reply routing for this session. Single-row table (id=1).
|
||||
-- Host overwrites on every container wake from the session's messaging_group
|
||||
-- and thread_id. Container reads it in send_message / ask_user_question /
|
||||
-- trigger_credential_collection to default the channel/thread of outbound
|
||||
-- messages when the agent doesn't specify an explicit destination.
|
||||
-- and thread_id. Container reads it in send_message / ask_user_question to
|
||||
-- default the channel/thread of outbound messages when the agent doesn't
|
||||
-- specify an explicit destination.
|
||||
CREATE TABLE IF NOT EXISTS session_routing (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
channel_type TEXT,
|
||||
|
||||
@@ -865,12 +865,6 @@ async function handleSystemAction(
|
||||
break;
|
||||
}
|
||||
|
||||
case 'request_credential': {
|
||||
const { handleCredentialRequest } = await import('./credentials.js');
|
||||
await handleCredentialRequest(content, session);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
log.warn('Unknown system action', { action });
|
||||
}
|
||||
|
||||
24
src/index.ts
24
src/index.ts
@@ -19,13 +19,6 @@ import {
|
||||
startOneCLIApprovalHandler,
|
||||
stopOneCLIApprovalHandler,
|
||||
} from './onecli-approvals.js';
|
||||
import {
|
||||
getCredentialForModal,
|
||||
handleCredentialChannelUnsupported,
|
||||
handleCredentialReject,
|
||||
handleCredentialSubmit,
|
||||
setCredentialDeliveryAdapter,
|
||||
} from './credentials.js';
|
||||
import { routeInbound } from './router.js';
|
||||
import {
|
||||
getPendingQuestion,
|
||||
@@ -93,22 +86,6 @@ async function main(): Promise<void> {
|
||||
log.error('Failed to handle question response', { questionId, err });
|
||||
});
|
||||
},
|
||||
getCredentialForModal,
|
||||
onCredentialReject(credentialId) {
|
||||
handleCredentialReject(credentialId).catch((err) =>
|
||||
log.error('Failed to handle credential reject', { credentialId, err }),
|
||||
);
|
||||
},
|
||||
onCredentialSubmit(credentialId, value) {
|
||||
handleCredentialSubmit(credentialId, value).catch((err) =>
|
||||
log.error('Failed to handle credential submit', { credentialId, err }),
|
||||
);
|
||||
},
|
||||
onCredentialChannelUnsupported(credentialId) {
|
||||
handleCredentialChannelUnsupported(credentialId).catch((err) =>
|
||||
log.error('Failed to handle credential channel-unsupported', { credentialId, err }),
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -135,7 +112,6 @@ async function main(): Promise<void> {
|
||||
},
|
||||
};
|
||||
setDeliveryAdapter(deliveryAdapter);
|
||||
setCredentialDeliveryAdapter(deliveryAdapter);
|
||||
|
||||
// 5. Start delivery polls
|
||||
startActiveDeliveryPoll();
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* OneCLI secrets facade.
|
||||
*
|
||||
* @onecli-sh/sdk 0.3.1 does not yet expose secret management. This module wraps
|
||||
* the `onecli secrets create` CLI so the rest of the codebase can call
|
||||
* `createSecret(...)` with the same shape we expect the SDK to ship with.
|
||||
*
|
||||
* When the SDK adds secret management, replace the body of `createSecret()`
|
||||
* with the SDK call and delete the CLI plumbing below. Nothing else in
|
||||
* NanoClaw should need to change — the public types here mirror the
|
||||
* anticipated SDK surface.
|
||||
*/
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
export interface CreateSecretInput {
|
||||
name: string;
|
||||
type: 'generic' | 'anthropic';
|
||||
value: string;
|
||||
hostPattern: string;
|
||||
pathPattern?: string;
|
||||
headerName?: string;
|
||||
valueFormat?: string;
|
||||
/**
|
||||
* Agent scoping. Not supported by current OneCLI CLI — included here so
|
||||
* callers can pass it today and it becomes live when the SDK adds scoping.
|
||||
*/
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export interface CreateSecretResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
hostPattern: string;
|
||||
}
|
||||
|
||||
export class OneCLISecretError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'OneCLISecretError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSecret(input: CreateSecretInput): Promise<CreateSecretResponse> {
|
||||
const payload: Record<string, unknown> = {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
value: input.value,
|
||||
hostPattern: input.hostPattern,
|
||||
};
|
||||
if (input.pathPattern) payload.pathPattern = input.pathPattern;
|
||||
if (input.headerName || input.valueFormat) {
|
||||
payload.injectionConfig = {
|
||||
...(input.headerName && { headerName: input.headerName }),
|
||||
...(input.valueFormat && { valueFormat: input.valueFormat }),
|
||||
};
|
||||
}
|
||||
|
||||
const stdout = await runOnecli(['secrets', 'create', '--json', JSON.stringify(payload)]);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch {
|
||||
throw new OneCLISecretError(`onecli returned non-JSON: ${stdout.slice(0, 200)}`);
|
||||
}
|
||||
const result = parsed as { id?: string; name?: string; hostPattern?: string; error?: string };
|
||||
if (result.error) throw new OneCLISecretError(result.error);
|
||||
return {
|
||||
id: result.id ?? '',
|
||||
name: result.name ?? input.name,
|
||||
hostPattern: result.hostPattern ?? input.hostPattern,
|
||||
};
|
||||
}
|
||||
|
||||
function runOnecli(args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('onecli', args, { timeout: 15_000 }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new OneCLISecretError(stderr || error.message));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
22
src/types.ts
22
src/types.ts
@@ -157,28 +157,6 @@ export interface PendingApproval {
|
||||
options_json: string;
|
||||
}
|
||||
|
||||
// ── Pending credentials (central DB) ──
|
||||
|
||||
export type PendingCredentialStatus = 'pending' | 'submitted' | 'saved' | 'rejected' | 'failed';
|
||||
|
||||
export interface PendingCredential {
|
||||
id: string;
|
||||
agent_group_id: string;
|
||||
session_id: string | null;
|
||||
name: string;
|
||||
type: 'generic' | 'anthropic';
|
||||
host_pattern: string;
|
||||
path_pattern: string | null;
|
||||
header_name: string | null;
|
||||
value_format: string | null;
|
||||
description: string | null;
|
||||
channel_type: string;
|
||||
platform_id: string;
|
||||
platform_message_id: string | null;
|
||||
status: PendingCredentialStatus;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ── Agent destinations (central DB) ──
|
||||
|
||||
export interface AgentDestination {
|
||||
|
||||
Reference in New Issue
Block a user