request_user_input
request_user_input
Metadata
Name
request_user_input
Updated
last month
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/requestUserInput.ts — request_user_input synthetic user-wait gate
*
* Third user-wait gate alongside `wait_for_user_ack` (button picker) and
* `present_preview` (multi-select card). This one captures **free-text input**
* from the user mid-plan and returns it as `{ value: string }`.
*
* ...
*/
import { z } from "zod";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "request_user_input",
description:
"Pauses the plan and captures a single free-text input from the user. " +
"Use when the agent needs a value (email, hostname, IP, account ID, etc.) " +
"that cannot be auto-detected and isn't covered by a button-picker " +
"(wait_for_user_ack, max 4 options) or a multi-select card (present_preview). " +
"Returns { value: string } — empty string on cancel/timeout. Routed " +
"through G4's runUserInputGate; runs OUTSIDE TOOL_TIMEOUT_MS.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
isUserWaitGate: true,
schema: {
prompt: z
.string()
.min(1)
.describe(
"Question shown to the user above the text-input field " +
"(e.g. 'What's your Okta email address?').",
),
placeholder: z
.string()
.optional()
.describe(
"Greyed-out hint text inside an EMPTY input. Disappears when " +
"the user starts typing. NOT submitted as a value if the user " +
"leaves the field blank. Use to suggest the expected format " +
"without pre-filling text — e.g. 'alice@example.com'.",
),
initialValue: z
.string()
.optional()
.describe(
"Text PRE-FILLED into the input. User can accept (click Continue) " +
"or edit. Submitted as the captured value if the user clicks " +
"Continue without editing. Use ONLY when the agent has a real " +
"best-guess (e.g. partial auto-detect result). Do NOT use as a " +
"placeholder substitute.",
),
validator: z
.string()
.optional()
.describe(
"Optional regex (string source, no leading/trailing slashes) " +
"the captured value must match before Continue enables. " +
"Example: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$' for email format. " +
"Validation happens client-side (button disabled until match) " +
"AND server-side at gate resolution (defense in depth). " +
"Server-side rejection returns empty string to the scratchpad.",
),
sensitive: z
.boolean()
.optional()
.describe(
"When true, the renderer shows a password-style masked input " +
"(dots), AND the captured value is redacted in audit logs, " +
"scratchpad replay, and consent-card previews. Use for tokens, " +
"MFA codes, recovery keys. Do NOT use for usernames or emails " +
"(masking those just frustrates the user when they want to " +
"verify they typed correctly).",
),
},
} as const;
// -- Stub run -----------------------------------------------------------------
/**
* Synthetic tool — never actually invoked through the normal pipeline.
* G4's executeStep() detects `meta.isUserWaitGate === true` and routes
* the step through `runUserInputGate()` in execution.ts instead. This
* stub exists only to satisfy the mcpTools registry's "every tool has
* a run() function" invariant.
*
* If this throws in production, something is wrong with the gate routing
* dispatcher — investigate `if (meta.isUserWaitGate)` in execution.ts.
*/
export async function run(): Promise<never> {
throw new Error(
"[request_user_input] This is a synthetic user-wait gate tool. " +
"G4 is expected to route steps whose tool.meta.isUserWaitGate is true " +
"through runUserInputGate() in execution.ts, NOT through the normal " +
"tool pipeline. If you see this error, the gate routing is broken.",
);
}
// -- CLI smoke test -----------------------------------------------------------
if (false) {
console.log(JSON.stringify({ meta: { ...meta, schema: "..." } }, null, 2));
}