mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(ui): use owner-sanitized setup receipts
This commit is contained in:
@@ -10872,7 +10872,7 @@ public struct SystemAgentChatResult: Codable, Sendable {
|
||||
public let reply: String
|
||||
public let sensitive: Bool?
|
||||
public let wizardinputpending: Bool?
|
||||
public let wizardactionaccepted: Bool?
|
||||
public let wizardaction: [String: AnyCodable]?
|
||||
public let action: AnyCodable
|
||||
public let agentdraft: String?
|
||||
public let agentid: String?
|
||||
@@ -10886,7 +10886,7 @@ public struct SystemAgentChatResult: Codable, Sendable {
|
||||
reply: String,
|
||||
sensitive: Bool? = nil,
|
||||
wizardinputpending: Bool? = nil,
|
||||
wizardactionaccepted: Bool? = nil,
|
||||
wizardaction: [String: AnyCodable]? = nil,
|
||||
action: AnyCodable,
|
||||
agentdraft: String? = nil,
|
||||
agentid: String? = nil,
|
||||
@@ -10899,7 +10899,7 @@ public struct SystemAgentChatResult: Codable, Sendable {
|
||||
self.reply = reply
|
||||
self.sensitive = sensitive
|
||||
self.wizardinputpending = wizardinputpending
|
||||
self.wizardactionaccepted = wizardactionaccepted
|
||||
self.wizardaction = wizardaction
|
||||
self.action = action
|
||||
self.agentdraft = agentdraft
|
||||
self.agentid = agentid
|
||||
@@ -10914,7 +10914,7 @@ public struct SystemAgentChatResult: Codable, Sendable {
|
||||
case reply
|
||||
case sensitive
|
||||
case wizardinputpending = "wizardInputPending"
|
||||
case wizardactionaccepted = "wizardActionAccepted"
|
||||
case wizardaction = "wizardAction"
|
||||
case action
|
||||
case agentdraft = "agentDraft"
|
||||
case agentid = "agentId"
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"79a4c99fa78209550335cbcf81b39ce261be41a90de04b64eb69526ef84349bc","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
{"contentHash":"f3bc029eef9d8e93bc73fb0869c9dbfbe4071f0b469f3bacc7018efb8a14ca0e","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"fdcb3e2536e7d4e3090322fac2793f497a0b6f331a8b583fa6156007721b177f","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
{"contentHash":"54a4c5b39ea87409b6cd7e417fe0cacbbcf4881ab4590642f3909ffe15fbc7c9","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"3bdfd31e7decd15278ea7864d8c16a0674be246e827b55b389593dcba3f2066c","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
{"contentHash":"ca0cf7a409663f6bc5b5fae0b5085a1e64fc4436f869f7960fab8e56708ffdaa","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"a5ff017a6e98b53dee31102591e7304d4d754b150b2a7684c257059921fa2523","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
{"contentHash":"9bfe9ccffb96f6291f04c744e4246581a681981fe07b8f3da579362c70fefb16","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"5d62b0618558e9a0093e4c8ad09b6cd143ed5b4c214de2eeefb255781a34c3bd","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
{"contentHash":"69a39e836698999eb6963c97480dead8ea95a4b2b5d50545e8fd3cb881a57596","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"32d2d5450bc2e08ebd3e2afc9302cf321469684f5997a059dddf28ace36675a8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
{"contentHash":"c7d3f4dbfc8b63cdcd6c1d7997e551b1b05177d38c1f58b65bed17fe8948eac9","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"3fea3804bec8232a55c95d3abb5bab8de5d8778652bddfafbb6362d27a4b3460","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
{"contentHash":"705111ce11d2b0570d78bedbfc0b3aeab0cc88997cad8994be063eb2dc5a2943","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"117093ae03e310c9ce4455f24e43fc678b8737af600eea1d0066137c2fa4651c","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
{"contentHash":"7a35b9299874b2cd93188087a3eb2b4c40d349a8dfc33100704e4876531a9a16","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"b5d69672e4813d9de0f99bf51d1baedd73b167a54126a0c984f65dd84945699c","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
{"contentHash":"7f7ad827672514d0d732fb17891851102a49d6b45904ee0b4815d5873cc88f2b","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"096282e453532f80d63ac296d2cc53ad65eb38f23703ea27e359cc215c7adfe4","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
|
||||
{"contentHash":"75dfa879ff76c5637813215d3ed4ff774f7bf5bd4998c2e74b504db1d48eea72","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"d93e8de2beeea1902998af40aae725e6d6318cb011810d3478a776a016f62723","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
{"contentHash":"8d9b1851d9d4bf3bc2273e5cf9b9071bedd205be3864d6fd4f88acf5f4097e27","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"60a4f0f0501fb64019a7aa9c3e66f64ce9c388fb38dadd69d71b64a467ecbcbd","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
{"contentHash":"3751be3212fc4f4d78555a72245a8492e0e5dfdfe30e7794cc20c22dd894e9df","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"283ada2bb6c4499207b08f1d23c80919daa4fd139c6e02810c2c15f7a90fec58","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
{"contentHash":"91cf8791fe2555a36a25bb359d1f14ef4ec2e61e25eb88af17187572ba431d50","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"e5b10b255ad2bd9ed54f10017e1e09440962ed846ba885059e13dab10b27811f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
{"contentHash":"4a6d7f93ea9d3a0bdb207e007aef6edc12d095d57d5b01a72020c3f4bf2f2a96","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"5055e33c4e0ffe0cb368d7a8e7970d5e97fd7d90913485e8a2bcecb1f97f92ef","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
{"contentHash":"3087ba183395692801b60510757304c2da517299748c10625ecc3321cbc0afdb","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"7f5f39112df30ff5d32513258437bef16e8304e01e1d61a50f754c8e26831cbc","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
{"contentHash":"694748a04e198f48a8e848d39b3c43942f8041f6353281d10d35f73f8d10f61b","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"983f60fcee19de0cc99dd2db216bea32e45054bffc50cfe3d6d382a7f51c7a41","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
{"contentHash":"749f32ab3cde36b570b13173a9286da5d9453bd3ae08f81087ff66fd834fe6cb","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
|
||||
@@ -15,7 +15,7 @@ version and the additive schema surface. Dates are authoring dates (2026).
|
||||
- Remove dead protocol surfaces and add since-vintage metadata to retained schemas and methods.
|
||||
- Add optional `step` on `SystemAgentChatResult` carrying the full awaited wizard step.
|
||||
- Add owner-bound active-wizard snapshots to Custodian chat history for reload recovery.
|
||||
- Add capability-gated accepted-action outcomes and receipt prompts to Custodian chat turns.
|
||||
- Add capability-gated, owner-sanitized action receipts to Custodian chat turns.
|
||||
|
||||
## Protocol v4 (current)
|
||||
|
||||
|
||||
@@ -92,16 +92,19 @@ describe("OpenClaw chat question protocol", () => {
|
||||
describe("OpenClaw chat result protocol", () => {
|
||||
const result = { sessionId: "session-1", reply: "Choose again.", action: "none" };
|
||||
|
||||
it("accepts explicit typed wizard-action outcomes", () => {
|
||||
it("accepts only sanitized accepted wizard-action metadata", () => {
|
||||
expect(Value.Check(SystemAgentChatResultSchema, result)).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: true }),
|
||||
Value.Check(SystemAgentChatResultSchema, {
|
||||
...result,
|
||||
wizardAction: { kind: "answer", prompt: "Choose a channel" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: false }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: "yes" }),
|
||||
Value.Check(SystemAgentChatResultSchema, {
|
||||
...result,
|
||||
wizardAction: { kind: "answer", value: "secret" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,12 @@ export const SystemAgentChatQuestionSchema = closedObject({
|
||||
skipAction: Type.Optional(Type.Literal("exit")),
|
||||
});
|
||||
|
||||
export const SystemAgentWizardActionReceiptSchema = closedObject({
|
||||
kind: Type.Union([Type.Literal("answer"), Type.Literal("cancel")]),
|
||||
/** Ordinary non-sensitive prompt copy used to label the receipt. */
|
||||
prompt: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** One OpenClaw reply; `action` tells clients about conversation handoffs. */
|
||||
export const SystemAgentChatResultSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
@@ -86,8 +92,8 @@ export const SystemAgentChatResultSchema = closedObject({
|
||||
sensitive: Type.Optional(Type.Boolean()),
|
||||
/** The hosted wizard will consume the next message as its current step answer. */
|
||||
wizardInputPending: Type.Optional(Type.Boolean()),
|
||||
/** Whether the submitted typed wizard action passed owner-side validation. */
|
||||
wizardActionAccepted: Type.Optional(Type.Boolean()),
|
||||
/** Sanitized receipt metadata for an accepted typed wizard action. */
|
||||
wizardAction: Type.Optional(SystemAgentWizardActionReceiptSchema),
|
||||
action: Type.Union([
|
||||
Type.Literal("none"),
|
||||
// The user asked to talk to their agent; clients should move to their
|
||||
@@ -115,18 +121,12 @@ export const SystemAgentChatHistoryParamsSchema = closedObject({
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const SystemAgentChatHistoryWizardActionSchema = closedObject({
|
||||
kind: Type.Union([Type.Literal("answer"), Type.Literal("cancel")]),
|
||||
/** Ordinary non-sensitive prompt copy used to label the receipt. */
|
||||
prompt: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export const SystemAgentChatHistoryTurnSchema = closedObject({
|
||||
role: Type.Union([Type.Literal("user"), Type.Literal("assistant")]),
|
||||
text: Type.String(),
|
||||
at: Type.Number(),
|
||||
/** Present only on accepted typed controls; values and sensitive prompts are excluded. */
|
||||
wizardAction: Type.Optional(SystemAgentChatHistoryWizardActionSchema),
|
||||
wizardAction: Type.Optional(SystemAgentWizardActionReceiptSchema),
|
||||
});
|
||||
|
||||
export const SystemAgentChatHistoryResultSchema = closedObject({
|
||||
@@ -389,9 +389,7 @@ export type SystemAgentWizardCancel = Static<typeof SystemAgentWizardCancelSchem
|
||||
export type SystemAgentChatQuestion = Static<typeof SystemAgentChatQuestionSchema>;
|
||||
export type SystemAgentChatResult = Static<typeof SystemAgentChatResultSchema>;
|
||||
export type SystemAgentChatHistoryParams = Static<typeof SystemAgentChatHistoryParamsSchema>;
|
||||
export type SystemAgentChatHistoryWizardAction = Static<
|
||||
typeof SystemAgentChatHistoryWizardActionSchema
|
||||
>;
|
||||
export type SystemAgentWizardActionReceipt = Static<typeof SystemAgentWizardActionReceiptSchema>;
|
||||
export type SystemAgentChatHistoryTurn = Static<typeof SystemAgentChatHistoryTurnSchema>;
|
||||
export type SystemAgentChatHistoryResult = Static<typeof SystemAgentChatHistoryResultSchema>;
|
||||
export type SystemChangeEntry = Static<typeof SystemChangeEntrySchema>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
validateSystemAgentChatHistoryParams,
|
||||
type SystemAgentChatHistoryTurn,
|
||||
type SystemAgentChatHistoryWizardAction,
|
||||
type SystemAgentWizardActionReceipt,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import type { SystemAgentChatEngine } from "../../system-agent/chat-engine.js";
|
||||
import { resolveSystemAgentDelegationKey } from "../../system-agent/delegation-session.js";
|
||||
@@ -41,7 +41,7 @@ export function persistSystemAgentEngineHistory(
|
||||
engine: Pick<SystemAgentChatEngine, "historySince">,
|
||||
startIndex: number,
|
||||
params: {
|
||||
wizardAction?: SystemAgentChatHistoryWizardAction;
|
||||
wizardAction?: SystemAgentWizardActionReceipt;
|
||||
wizardActionAccepted?: boolean;
|
||||
} = {},
|
||||
): SystemAgentChatHistoryTurn[] {
|
||||
|
||||
@@ -116,28 +116,40 @@ describe("system-agent chat input", () => {
|
||||
expect(handle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves the enriched wizard step in the gateway result", () => {
|
||||
it("forwards only owner-accepted wizard receipt metadata", () => {
|
||||
const rejected = buildSystemAgentChatResult({
|
||||
sessionId: "s1",
|
||||
reply: {
|
||||
text: "Choose a channel.",
|
||||
action: "none",
|
||||
wizardActionAccepted: false,
|
||||
wizardAction: { kind: "answer", prompt: "Channel" },
|
||||
step: {
|
||||
id: "channel",
|
||||
type: "select",
|
||||
message: "Channel",
|
||||
options: [{ label: "Twitch", value: "twitch" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(rejected).toMatchObject({
|
||||
sessionId: "s1",
|
||||
reply: "Choose a channel.",
|
||||
action: "none",
|
||||
step: { id: "channel", type: "select" },
|
||||
});
|
||||
expect(rejected).not.toHaveProperty("wizardAction");
|
||||
|
||||
expect(
|
||||
buildSystemAgentChatResult({
|
||||
sessionId: "s1",
|
||||
reply: {
|
||||
text: "Choose a channel.",
|
||||
text: "Next step.",
|
||||
action: "none",
|
||||
wizardActionAccepted: false,
|
||||
step: {
|
||||
id: "channel",
|
||||
type: "select",
|
||||
message: "Channel",
|
||||
options: [{ label: "Twitch", value: "twitch" }],
|
||||
},
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Channel" },
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
sessionId: "s1",
|
||||
reply: "Choose a channel.",
|
||||
action: "none",
|
||||
wizardActionAccepted: false,
|
||||
step: { id: "channel", type: "select" },
|
||||
});
|
||||
}).wizardAction,
|
||||
).toEqual({ kind: "answer", prompt: "Channel" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,8 +82,8 @@ export function buildSystemAgentChatResult(params: {
|
||||
: {}),
|
||||
...(params.reply.sensitive === true ? { sensitive: true } : {}),
|
||||
...(params.reply.wizardInputPending === true ? { wizardInputPending: true } : {}),
|
||||
...(params.reply.wizardActionAccepted !== undefined
|
||||
? { wizardActionAccepted: params.reply.wizardActionAccepted }
|
||||
...(params.reply.wizardActionAccepted === true && params.reply.wizardAction
|
||||
? { wizardAction: params.reply.wizardAction }
|
||||
: {}),
|
||||
...(params.reply.question ? { question: params.reply.question } : {}),
|
||||
...(params.reply.step ? { step: params.reply.step } : {}),
|
||||
|
||||
@@ -243,18 +243,18 @@ describe("openclaw.chat reset boundary", () => {
|
||||
sessionId: "recover-session",
|
||||
wizardAnswer: { stepId: portStepId, value: "banana" },
|
||||
});
|
||||
expect(rejected.wizardActionAccepted).toBe(false);
|
||||
expect(rejected).not.toHaveProperty("wizardAction");
|
||||
const accepted = await sendChat({
|
||||
sessionId: "recover-session",
|
||||
wizardAnswer: { stepId: portStepId, value: "18789" },
|
||||
});
|
||||
expect(accepted.wizardActionAccepted).toBe(true);
|
||||
expect(accepted.wizardAction).toEqual({ kind: "answer", prompt: "Port" });
|
||||
const secretStepId = expectDefined(accepted.step?.id, "expected sensitive step");
|
||||
const cancelled = await sendChat({
|
||||
sessionId: "recover-session",
|
||||
wizardCancel: { stepId: secretStepId },
|
||||
});
|
||||
expect(cancelled.wizardActionAccepted).toBe(true);
|
||||
expect(cancelled.wizardAction).toEqual({ kind: "cancel" });
|
||||
|
||||
closeOpenClawStateDatabase();
|
||||
sessions.clear();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
SystemAgentChatQuestion,
|
||||
SystemAgentChatHistoryWizardAction,
|
||||
SystemAgentWizardActionReceipt,
|
||||
SystemAgentWizardCancel,
|
||||
WizardAnswer,
|
||||
} from "../../packages/gateway-protocol/src/index.js";
|
||||
@@ -33,7 +33,7 @@ export type SystemAgentChatReply = {
|
||||
wizardInputPending?: boolean;
|
||||
/** The submitted typed wizard action passed or failed owner-side validation. */
|
||||
wizardActionAccepted?: boolean;
|
||||
wizardAction?: SystemAgentChatHistoryWizardAction;
|
||||
wizardAction?: SystemAgentWizardActionReceipt;
|
||||
handoff?: SystemAgentOperation;
|
||||
question?: SystemAgentChatQuestion;
|
||||
step?: WizardStep;
|
||||
@@ -48,13 +48,13 @@ export type ChatWizardResult = {
|
||||
export type ChatWizardAnswerResult = ChatWizardResult & {
|
||||
accepted: boolean;
|
||||
userHistoryText: string;
|
||||
wizardAction: SystemAgentChatHistoryWizardAction;
|
||||
wizardAction: SystemAgentWizardActionReceipt;
|
||||
};
|
||||
|
||||
function wizardActionReceipt(
|
||||
step: WizardStep,
|
||||
kind: SystemAgentChatHistoryWizardAction["kind"],
|
||||
): SystemAgentChatHistoryWizardAction {
|
||||
kind: SystemAgentWizardActionReceipt["kind"],
|
||||
): SystemAgentWizardActionReceipt {
|
||||
const prompt =
|
||||
!step.sensitive && !step.deviceCode && !step.externalUrl
|
||||
? (step.title ?? step.message)
|
||||
|
||||
@@ -442,7 +442,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Choose features.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Which channel?" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "features",
|
||||
@@ -466,7 +466,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Enter the secret.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Which features?" },
|
||||
sensitive: true,
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
@@ -493,7 +493,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Name this connection.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "label",
|
||||
@@ -528,7 +528,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Confirm setup.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Connection name" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "confirm",
|
||||
@@ -563,7 +563,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Setup complete.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Connect Twitch now?" },
|
||||
});
|
||||
await page.getByText("Setup complete.").waitFor();
|
||||
expect(
|
||||
@@ -706,7 +706,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-reload-wizard",
|
||||
reply: "Twitch setup cancelled.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "cancel" },
|
||||
});
|
||||
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await page.getByText("Twitch setup cancelled.").waitFor();
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("Custodian wizard reload recovery", () => {
|
||||
sessionId: "live-wizard",
|
||||
reply: "Twitch setup cancelled.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "cancel" },
|
||||
};
|
||||
}
|
||||
freshChatCount += 1;
|
||||
|
||||
@@ -19,10 +19,7 @@ describe("custodian structured wizard", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ accepted: false, result: "rejected" },
|
||||
{ accepted: undefined, result: "ambiguous" },
|
||||
])("keeps a $result typed answer active as a plain user turn", async ({ accepted }) => {
|
||||
it("keeps a rejected typed answer active as a plain user turn", async () => {
|
||||
const step = {
|
||||
id: "port",
|
||||
type: "text" as const,
|
||||
@@ -41,7 +38,6 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: "validation-session",
|
||||
reply: "Enter port 18789.",
|
||||
action: "none",
|
||||
...(accepted === undefined ? {} : { wizardActionAccepted: accepted }),
|
||||
wizardInputPending: true,
|
||||
step,
|
||||
});
|
||||
@@ -131,7 +127,7 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: string;
|
||||
reply: string;
|
||||
action: "none";
|
||||
wizardActionAccepted: boolean;
|
||||
wizardAction: { kind: "answer"; prompt: string };
|
||||
}>();
|
||||
const step = {
|
||||
id: "port",
|
||||
@@ -185,7 +181,7 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: "rotation-session",
|
||||
reply: "Accepted by the retired client.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Gateway port" },
|
||||
});
|
||||
|
||||
await waitForFast(() => expect(page.textContent).toContain("Gateway connection changed"));
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("custodian page", () => {
|
||||
sessionId: "rich-wizard-session",
|
||||
reply: "Choose features.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Which channel?" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "features",
|
||||
@@ -117,7 +117,7 @@ describe("custodian page", () => {
|
||||
sessionId: "rich-wizard-session",
|
||||
reply: "Enter the secret.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Which features?" },
|
||||
sensitive: true,
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
@@ -131,7 +131,7 @@ describe("custodian page", () => {
|
||||
sessionId: "rich-wizard-session",
|
||||
reply: "Setup complete.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer" },
|
||||
});
|
||||
const { context } = createContext(request);
|
||||
const { page } = await mountPage(context);
|
||||
@@ -206,6 +206,11 @@ describe("custodian page", () => {
|
||||
expect(page.textContent).toContain("Chat, Announcements");
|
||||
expect(page.textContent).toContain("Sensitive reply sent");
|
||||
expect(page.querySelectorAll(".custodian__structured-response")).toHaveLength(3);
|
||||
expect(
|
||||
[...page.querySelectorAll(".custodian__structured-response-prompt")].map(
|
||||
(element) => element.textContent,
|
||||
),
|
||||
).toEqual(["Which channel?", "Which features?", "Setup answer"]);
|
||||
expect(page.querySelector(".chat-group.user")).toBeNull();
|
||||
expect(page.textContent).not.toContain("fake-client-secret");
|
||||
expect(page.querySelector(".agent-chat__composer-shell")).not.toBeNull();
|
||||
@@ -231,7 +236,7 @@ describe("custodian page", () => {
|
||||
sessionId: "cancel-wizard-session",
|
||||
reply: "Channel setup cancelled.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "cancel" },
|
||||
});
|
||||
const { context } = createContext(request);
|
||||
const { page } = await mountPage(context);
|
||||
@@ -256,6 +261,9 @@ describe("custodian page", () => {
|
||||
expect(page.querySelector(".custodian__wizard-step")).toBeNull();
|
||||
expect(page.querySelector(".chat-group.user")).toBeNull();
|
||||
expect(page.querySelector(".custodian__structured-response")?.textContent).toContain("Cancel");
|
||||
expect(page.querySelector(".custodian__structured-response-prompt")?.textContent).toBe(
|
||||
"Setup answer",
|
||||
);
|
||||
expect(page.querySelector(".custodian__structured-response")?.textContent).toContain(
|
||||
"Setup cancelled",
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
readSystemAgentInferenceUnavailableErrorDetails,
|
||||
type SystemAgentChatParams,
|
||||
type SystemAgentChatResult,
|
||||
type SystemAgentWizardActionReceipt,
|
||||
} from "@openclaw/gateway-protocol";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { selectApplicationSession } from "../../app/agent-selection.ts";
|
||||
@@ -55,6 +56,10 @@ function hasCustodianWizardAction(params: SystemAgentChatParams): boolean {
|
||||
type StoreListener = () => void;
|
||||
type ConfiguredInferenceState = "unresolved" | "required" | "ready";
|
||||
type CustodianSetupIssue = "missing" | "unavailable";
|
||||
type CustodianSendResult = {
|
||||
outcome: eventNudgeState.CustodianSendOutcome;
|
||||
wizardAction?: SystemAgentWizardActionReceipt;
|
||||
};
|
||||
|
||||
/** One process-local conversation owner shared by the full page and dock surface. */
|
||||
export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
@@ -228,7 +233,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
return "rejected";
|
||||
}
|
||||
const displayText = this.sensitive ? t("custodian.sensitiveReply") : (display ?? message);
|
||||
return await this.sendUserTurn(
|
||||
const result = await this.sendUserTurn(
|
||||
client,
|
||||
{
|
||||
sessionId: this.sessionId,
|
||||
@@ -237,6 +242,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
displayText,
|
||||
questionReply,
|
||||
);
|
||||
return result.outcome;
|
||||
}
|
||||
|
||||
private async sendUserTurn(
|
||||
@@ -245,7 +251,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
displayText: string,
|
||||
questionReply: boolean,
|
||||
userTurnProjection: "always" | "unless-accepted" = "always",
|
||||
): Promise<eventNudgeState.CustodianSendOutcome> {
|
||||
): Promise<CustodianSendResult> {
|
||||
const questionState = [this.answeredQuestions, this.questionReplyUncertain] as const;
|
||||
if (questionReply) {
|
||||
this.questionReplyUncertain = true;
|
||||
@@ -269,7 +275,8 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
this.emit();
|
||||
const reply = this.requestReply(client, params);
|
||||
const replyEpoch = this.requestEpoch;
|
||||
const outcome = await reply;
|
||||
const result = await reply;
|
||||
const outcome = result.outcome;
|
||||
if (userTurnProjection === "unless-accepted" && outcome !== "accepted") {
|
||||
// Owner-confirmed acceptance replaces the attempted turn with a receipt. Every other
|
||||
// outcome keeps the masked display in its original transcript position.
|
||||
@@ -285,7 +292,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
}
|
||||
this.emit();
|
||||
}
|
||||
return outcome;
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendEventNudge(): Promise<void> {
|
||||
@@ -637,17 +644,17 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
private async requestReply(
|
||||
client: GatewayBrowserClient,
|
||||
params: SystemAgentChatParams,
|
||||
): Promise<eventNudgeState.CustodianSendOutcome> {
|
||||
): Promise<CustodianSendResult> {
|
||||
const context = this.context;
|
||||
if (!context) {
|
||||
return "rejected";
|
||||
return { outcome: "rejected" };
|
||||
}
|
||||
const snapshot = context.gateway.snapshot;
|
||||
if (
|
||||
snapshot.client !== client ||
|
||||
!canCallGatewayMethod(snapshot, "openclaw.chat", "operator.admin")
|
||||
) {
|
||||
return "rejected";
|
||||
return { outcome: "rejected" };
|
||||
}
|
||||
this.requestAbort?.abort();
|
||||
const requestAbort = new AbortController();
|
||||
@@ -669,7 +676,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
});
|
||||
delivery = "received";
|
||||
if (epoch !== this.requestEpoch || client !== this.activeClient) {
|
||||
return "sent";
|
||||
return { outcome: "sent" };
|
||||
}
|
||||
this.sessionId = result.sessionId;
|
||||
this.sensitive = result.sensitive === true;
|
||||
@@ -690,7 +697,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
if (result.agentId) {
|
||||
const roster = await context.agents.refreshList();
|
||||
if (epoch !== this.requestEpoch || client !== this.activeClient) {
|
||||
return "sent";
|
||||
return { outcome: "sent" };
|
||||
}
|
||||
sessionKey = buildAgentMainSessionKey({
|
||||
agentId: result.agentId,
|
||||
@@ -715,9 +722,11 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
this.exitSetup();
|
||||
}
|
||||
if (!hasCustodianWizardAction(params) || !this.wizardActionReceiptsAvailable) {
|
||||
return "sent";
|
||||
return { outcome: "sent" };
|
||||
}
|
||||
return result.wizardActionAccepted === true ? "accepted" : "rejected";
|
||||
return result.wizardAction
|
||||
? { outcome: "accepted", wizardAction: result.wizardAction }
|
||||
: { outcome: "rejected" };
|
||||
} catch (error) {
|
||||
if (epoch === this.requestEpoch && client === this.activeClient) {
|
||||
this.error = custodianErrorMessage(error);
|
||||
@@ -739,7 +748,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
// User turns have no idempotency key and are never replayed after an ambiguous failure.
|
||||
this.retryParams = null;
|
||||
}
|
||||
return eventNudgeState.classifyCustodianSendFailure(error, delivery);
|
||||
return { outcome: eventNudgeState.classifyCustodianSendFailure(error, delivery) };
|
||||
} finally {
|
||||
if (this.requestAbort === requestAbort) {
|
||||
this.requestAbort = null;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { SystemAgentChatParams } from "@openclaw/gateway-protocol";
|
||||
import type {
|
||||
SystemAgentChatParams,
|
||||
SystemAgentWizardActionReceipt,
|
||||
} from "@openclaw/gateway-protocol";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { custodianWizardSubmission } from "./custodian-wizard-step.ts";
|
||||
@@ -26,7 +29,10 @@ type StructuredInteractionHost = {
|
||||
params: SystemAgentChatParams,
|
||||
display: string,
|
||||
userTurnProjection: "always" | "unless-accepted",
|
||||
) => Promise<eventNudgeState.CustodianSendOutcome>;
|
||||
) => Promise<{
|
||||
outcome: eventNudgeState.CustodianSendOutcome;
|
||||
wizardAction?: SystemAgentWizardActionReceipt;
|
||||
}>;
|
||||
};
|
||||
|
||||
function withResponse(
|
||||
@@ -49,7 +55,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
request: SystemAgentChatParams;
|
||||
display: string;
|
||||
kind: CustodianStructuredResponse["kind"];
|
||||
prompt?: string;
|
||||
}): Promise<eventNudgeState.CustodianSendOutcome> => {
|
||||
const state = host.state();
|
||||
if (
|
||||
@@ -67,36 +72,35 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
display: params.display,
|
||||
kind: params.kind,
|
||||
state: "submitting",
|
||||
...(params.prompt ? { prompt: params.prompt } : {}),
|
||||
}) ?? [...state.messages],
|
||||
);
|
||||
host.emit();
|
||||
}
|
||||
const outcome = await host.sendUserTurn(
|
||||
const result = await host.sendUserTurn(
|
||||
params.client,
|
||||
params.request,
|
||||
params.display,
|
||||
state.wizardActionReceiptsAvailable ? "unless-accepted" : "always",
|
||||
);
|
||||
if (!state.wizardActionReceiptsAvailable) {
|
||||
return outcome;
|
||||
return result.outcome;
|
||||
}
|
||||
const current = host.state();
|
||||
const response: CustodianStructuredResponse | null =
|
||||
outcome !== "accepted"
|
||||
result.outcome !== "accepted" || !result.wizardAction
|
||||
? null
|
||||
: {
|
||||
display: params.display,
|
||||
kind: params.kind,
|
||||
kind: result.wizardAction.kind,
|
||||
state: "submitted",
|
||||
...(params.prompt ? { prompt: params.prompt } : {}),
|
||||
...(result.wizardAction?.prompt ? { prompt: result.wizardAction.prompt } : {}),
|
||||
};
|
||||
const messages = withResponse(current.messages, params.message.id, response);
|
||||
if (messages) {
|
||||
host.replaceMessages(messages);
|
||||
host.emit();
|
||||
}
|
||||
return outcome;
|
||||
return result.outcome;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -113,7 +117,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
request: { sessionId: state.sessionId, wizardAnswer: submission.answer },
|
||||
display: message.step?.sensitive ? t("custodian.sensitiveReply") : submission.display,
|
||||
kind: "answer",
|
||||
prompt: message.step?.title ?? message.step?.message,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -137,7 +140,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
request: { sessionId: state.sessionId, wizardCancel: { stepId: step.id } },
|
||||
display: t("custodian.cancel"),
|
||||
kind: "cancel",
|
||||
prompt: step.title ?? step.message,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -215,13 +215,7 @@ function renderCustodianEarlierDivider(message: CustodianMessage, boundaryAfterI
|
||||
}
|
||||
|
||||
function structuredPrompt(message: CustodianMessage): string {
|
||||
return (
|
||||
message.structuredResponse?.prompt ??
|
||||
message.step?.title ??
|
||||
message.step?.message ??
|
||||
message.question?.question ??
|
||||
t("custodian.structured.response")
|
||||
);
|
||||
return message.structuredResponse?.prompt ?? t("custodian.structured.response");
|
||||
}
|
||||
|
||||
function renderStructuredResponse(message: CustodianMessage) {
|
||||
|
||||
Reference in New Issue
Block a user