openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
328 lines (327 loc) • 14.8 kB
JavaScript
import { n as ENV_SECRET_REF_ID_RE } from "./types.secrets-kC0nOetj.js";
import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js";
import { n as SecretStoreValidationError } from "./secret-store-validation-error-Bzzf_1MN.js";
import { i as listSecretStoreEntries } from "./secret-store-CC1e6gjb.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { _g as formatValidationErrors, ar as validateQuestionListParams, cr as validateQuestionWaitAnswerParams, ir as validateQuestionGetParams, or as validateQuestionRequestParams, sr as validateQuestionResolveParams } from "./src-BiL5aQto.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-8xEjWSNi.js";
import { p as registerActiveEmbeddedRunHumanInputWait } from "./run-state-CK-dxBV7.js";
import { n as handleQuestionChannelResolved, t as handleQuestionChannelRequested } from "./question-channel-runtime-Ds-274X0.js";
import { n as hasOperatorBoundary } from "./operator-role-policy-wsr1DeJv.js";
import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js";
import { F as isGatewayAdmin, M as authorizeSessionSharingTarget, j as authorizeSessionSharing, r as createSessionListEntryFilter, z as resolveSessionSharingTarget } from "./session-sharing-B7MI8hNo.js";
import { n as QuestionManagerError, r as QuestionManagerErrorCodes } from "./question-manager-CpjwI9hn.js";
//#region src/gateway/server-methods/question.ts
const DEFAULT_QUESTION_TIMEOUT_MS = 9e5;
var QuestionRequestValidationError = class extends Error {};
function validationError(method, errors, respond) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid ${method} params: ${formatValidationErrors(errors)}`));
}
function managerError(error, respond) {
if (!(error instanceof QuestionManagerError)) return false;
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message, { details: { reason: error.code } }));
return true;
}
function questionNotFound(id) {
return errorShape(ErrorCodes.INVALID_REQUEST, `question '${id}' was not found`, { details: { reason: QuestionManagerErrorCodes.NOT_FOUND } });
}
function authorizeQuestionRecord(params) {
if (isGatewayAdmin(params.client) || !hasOperatorBoundary(params.client, params.cfg) || !params.question.sessionKey) return null;
const target = resolveSessionSharingTarget({
cfg: params.cfg,
sessionKey: params.question.sessionKey,
agentId: params.question.agentId
});
const canSeeSession = target && (createSessionListEntryFilter({
cfg: params.cfg,
client: params.client
})?.(target.canonicalKey, target.entry) ?? true);
if (!target || !canSeeSession) return questionNotFound(params.question.id);
return params.access === "mutate" ? authorizeSessionSharingTarget({
cfg: params.cfg,
client: params.client,
target
}) : null;
}
function normalizeQuestions(params) {
const ids = /* @__PURE__ */ new Set();
return params.questions.map((question) => {
if (ids.has(question.questionId)) throw new QuestionRequestValidationError(`duplicate question id '${question.questionId}'`);
ids.add(question.questionId);
if (question.options.length === 1) throw new QuestionRequestValidationError(`question '${question.questionId}' must have either no options or 2 to 4 options`);
const binding = question.secretStore;
if (question.isSecret && !binding) throw new QuestionRequestValidationError(`question '${question.questionId}': secret questions are not supported yet`);
if (binding) {
if (!question.isSecret) throw new QuestionRequestValidationError(`question '${question.questionId}': secret store binding requires a secret question`);
if (params.questions.length !== 1 || question.options.length !== 0 || question.multiSelect) throw new QuestionRequestValidationError(`question '${question.questionId}': secret store requests require one free-text, single-select question`);
if (!ENV_SECRET_REF_ID_RE.test(binding.name)) throw new QuestionRequestValidationError(`question '${question.questionId}': invalid secret store entry name`);
if (binding.kind !== "secret") throw new QuestionRequestValidationError(`question '${question.questionId}': masked requests require kind "secret"; set environment values in Settings or the CLI`);
if ((binding.allowedHosts?.length ?? 0) > 128) throw new QuestionRequestValidationError(`question '${question.questionId}': secret store allowed hosts exceed the limit`);
const existing = listSecretStoreEntries({ scope: { kind: "team" } }).find((entry) => entry.name === binding.name);
return {
...question,
secretStore: {
...binding,
allowedHosts: binding.allowedHosts ?? existing?.allowedHosts ?? []
},
...existing ? { secretStoreExisting: {
updatedAtMs: existing.updatedAtMs,
...existing.updatedBy ? { updatedBy: existing.updatedBy } : {}
} } : {}
};
}
const optionLabels = /* @__PURE__ */ new Set();
for (const option of question.options) {
const normalizedLabel = option.label.trim().toLowerCase();
if (optionLabels.has(normalizedLabel)) throw new QuestionRequestValidationError(`question '${question.questionId}' has duplicate option label '${option.label}'`);
optionLabels.add(normalizedLabel);
}
return question;
});
}
/** Creates the lazily loaded question RPC surface for one Gateway lifetime. */
function createQuestionHandlers(manager, storeWriteService) {
return {
"question.request": ({ params, respond, context, client }) => {
if (!validateQuestionRequestParams(params)) {
validationError("question.request", validateQuestionRequestParams.errors, respond);
return;
}
let request = params;
const storeBound = request.questions.some((question) => question.secretStore);
if (storeBound && !isGatewayAdmin(client)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "secret store questions require an operator.admin client"));
return;
}
const identity = client?.internal?.agentRuntimeIdentity;
const validateAuthority = context.validateAgentRuntimeApprovalAuthority;
if (storeBound && (!identity || !validateAuthority)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "secret store questions require trusted agent runtime authority"));
return;
}
const requester = identity ? structuredClone(identity) : void 0;
const isRequesterActive = requester && validateAuthority ? () => {
try {
return validateAuthority(requester);
} catch {
return false;
}
} : void 0;
if (requester) request = {
...request,
agentId: requester.agentId,
sessionKey: requester.sessionKey,
runId: requester.operationalRunInstance.runId
};
try {
const requestedSession = request.sessionKey ? resolveRequestedSessionAgentId(context.getRuntimeConfig(), request.sessionKey, request.agentId) : void 0;
if (requestedSession && !requestedSession.ok) {
respond(false, void 0, requestedSession.error);
return;
}
const sessionKey = request.sessionKey && requestedSession?.ok ? resolveStoredSessionKeyForAgentStore({
cfg: context.getRuntimeConfig(),
agentId: requestedSession.agentId,
sessionKey: request.sessionKey
}) : void 0;
if (sessionKey && hasOperatorBoundary(client, context.getRuntimeConfig())) {
const authorizationError = authorizeSessionSharing({
cfg: context.getRuntimeConfig(),
client,
sessionKey,
agentId: requestedSession?.ok ? requestedSession.agentId : void 0
});
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
}
const record = manager.request({
...request.id ? { id: request.id } : {},
questions: normalizeQuestions(request),
...requestedSession?.ok ? { agentId: requestedSession.agentId } : request.agentId ? { agentId: request.agentId } : {},
...sessionKey ? { sessionKey } : {},
...request.runId ? { runId: request.runId } : {},
timeoutMs: request.timeoutMs ?? DEFAULT_QUESTION_TIMEOUT_MS,
isRequesterActive,
registerHumanInputWait: requester && isRequesterActive ? (isPending) => registerActiveEmbeddedRunHumanInputWait(requester.delegatedAuthority, isPending) : void 0,
onResolved: (event) => {
handleQuestionChannelResolved(event);
if (sessionKey && context.getRuntimeConfig().gateway?.roles) context.broadcast("question.resolved", event, {
sessionKeys: [sessionKey],
...requestedSession?.ok ? { agentId: requestedSession.agentId } : {}
});
else context.broadcast("question.resolved", event);
}
});
handleQuestionChannelRequested(record);
if (sessionKey && context.getRuntimeConfig().gateway?.roles) context.broadcast("question.requested", record, {
sessionKeys: [sessionKey],
...requestedSession?.ok ? { agentId: requestedSession.agentId } : {}
});
else context.broadcast("question.requested", record);
respond(true, {
id: record.id,
expiresAtMs: record.expiresAtMs
}, void 0);
} catch (error) {
if (error instanceof QuestionRequestValidationError) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message));
return;
}
if (!managerError(error, respond)) {
if (storeBound) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "Secret store entry metadata is unavailable."));
return;
}
throw error;
}
}
},
"question.waitAnswer": async ({ params, respond, client, context }) => {
if (!validateQuestionWaitAnswerParams(params)) {
validationError("question.waitAnswer", validateQuestionWaitAnswerParams.errors, respond);
return;
}
const request = params;
try {
const question = manager.get(request.id);
if (question) {
const authorizationError = authorizeQuestionRecord({
cfg: context.getRuntimeConfig(),
client,
question,
access: "read"
});
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
}
const answer = await manager.waitAnswer(request.id, request.timeoutMs, request.includeResolutionId);
if (question) {
const authorizationError = authorizeQuestionRecord({
cfg: context.getRuntimeConfig(),
client,
question,
access: "read"
});
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
}
respond(true, answer, void 0);
} catch (error) {
if (!managerError(error, respond)) throw error;
}
},
"question.resolve": async ({ params, respond, client, context }) => {
if (!validateQuestionResolveParams(params)) {
validationError("question.resolve", validateQuestionResolveParams.errors, respond);
return;
}
const request = params;
try {
const question = manager.get(request.id);
if (question) {
const authorizationError = authorizeQuestionRecord({
cfg: context.getRuntimeConfig(),
client,
question,
access: "mutate"
});
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
}
if ("cancel" in request) {
respond(true, manager.cancel(request.id, request.resolvedBy), void 0);
return;
}
const secretQuestion = question?.questions[0];
const binding = secretQuestion?.secretStore;
if (!binding || !question) {
if (request.secretStoreAllowedHosts !== void 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Secret store allowed hosts require a store-bound question."));
return;
}
respond(true, manager.resolve(request.id, request.answers, request.resolvedBy, { resolutionId: request.resolutionId }), void 0);
return;
}
const submittedAnswers = request.answers.answers;
const values = Object.hasOwn(submittedAnswers, secretQuestion.questionId) ? submittedAnswers[secretQuestion.questionId] : void 0;
const value = values?.[0];
if (Object.keys(submittedAnswers).length !== 1 || values?.length !== 1 || value === void 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `question '${secretQuestion.questionId}' requires exactly one secret value`));
return;
}
registerSecretValueForRedaction(value);
const allowedHosts = request.secretStoreAllowedHosts ?? binding.allowedHosts;
let saved = false;
try {
const result = manager.resolve(request.id, { answers: { [secretQuestion.questionId]: ["stored"] } }, request.resolvedBy, {
resolutionId: request.resolutionId,
commit: () => {
storeWriteService.write({
name: binding.name,
value,
kind: "secret",
...allowedHosts !== void 0 ? { allowedHosts } : {},
updatedBy: storeWriteService.resolveUpdatedBy(client)
});
saved = true;
}
});
await storeWriteService.reloadReference(binding.name);
respond(true, result, void 0);
} catch (error) {
if (managerError(error, respond)) return;
respond(false, void 0, errorShape(!saved && error instanceof SecretStoreValidationError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, saved ? "Secret store entry was saved, but runtime refresh failed. Resolve provider errors and retry secrets.reload; do not resubmit this answer." : error instanceof SecretStoreValidationError ? error.message : "Secret store entry could not be saved."));
}
} catch (error) {
if (!managerError(error, respond)) throw error;
}
},
"question.get": ({ params, respond, client, context }) => {
if (!validateQuestionGetParams(params)) {
validationError("question.get", validateQuestionGetParams.errors, respond);
return;
}
const id = params.id;
const question = manager.get(id);
if (!question) {
respond(false, void 0, questionNotFound(id));
return;
}
const authorizationError = authorizeQuestionRecord({
cfg: context.getRuntimeConfig(),
client,
question,
access: "read"
});
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
respond(true, { question }, void 0);
},
"question.list": ({ params, respond, client, context }) => {
if (!validateQuestionListParams(params)) {
validationError("question.list", validateQuestionListParams.errors, respond);
return;
}
const cfg = context.getRuntimeConfig();
respond(true, { questions: manager.list().filter((question) => !authorizeQuestionRecord({
cfg,
client,
question,
access: "read"
})) }, void 0);
}
};
}
//#endregion
export { createQuestionHandlers };