Add explicit ownerDisplaySecret for owner ID hash obfuscation (#22520)
* feat(config): add owner display secret setting * feat(prompt): add explicit owner hash secret to obfuscation path * test(prompt): assert owner hash secret mode behavior * Update src/agents/system-prompt.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
parent
fe609c0c77
commit
9abab6a2c9
@ -86,6 +86,11 @@ export function buildSystemPrompt(params: {
|
||||
defaultThinkLevel: params.defaultThinkLevel,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: params.config?.commands?.ownerDisplay,
|
||||
ownerDisplaySecret:
|
||||
params.config?.commands?.ownerDisplaySecret ??
|
||||
params.config?.gateway?.auth?.token ??
|
||||
params.config?.gateway?.remote?.token,
|
||||
reasoningTagHint: false,
|
||||
heartbeatPrompt: params.heartbeatPrompt,
|
||||
docsPath: params.docsPath,
|
||||
|
||||
@ -484,6 +484,11 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: params.config?.commands?.ownerDisplay,
|
||||
ownerDisplaySecret:
|
||||
params.config?.commands?.ownerDisplaySecret ??
|
||||
params.config?.gateway?.auth?.token ??
|
||||
params.config?.gateway?.remote?.token,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: isDefaultAgent
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
|
||||
@ -443,6 +443,11 @@ export async function runEmbeddedAttempt(
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: params.config?.commands?.ownerDisplay,
|
||||
ownerDisplaySecret:
|
||||
params.config?.commands?.ownerDisplaySecret ??
|
||||
params.config?.gateway?.auth?.token ??
|
||||
params.config?.gateway?.remote?.token,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: isDefaultAgent
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
|
||||
@ -14,6 +14,8 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
extraSystemPrompt?: string;
|
||||
ownerNumbers?: string[];
|
||||
ownerDisplay?: "raw" | "hash";
|
||||
ownerDisplaySecret?: string;
|
||||
reasoningTagHint: boolean;
|
||||
heartbeatPrompt?: string;
|
||||
skillsPrompt?: string;
|
||||
@ -55,6 +57,8 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: params.ownerDisplay,
|
||||
ownerDisplaySecret: params.ownerDisplaySecret,
|
||||
reasoningTagHint: params.reasoningTagHint,
|
||||
heartbeatPrompt: params.heartbeatPrompt,
|
||||
skillsPrompt: params.skillsPrompt,
|
||||
|
||||
@ -16,6 +16,45 @@ describe("buildAgentSystemPrompt", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("hashes owner numbers when ownerDisplay is hash", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
ownerNumbers: ["+123", "+456", ""],
|
||||
ownerDisplay: "hash",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Authorized Senders");
|
||||
expect(prompt).toContain("Authorized senders:");
|
||||
expect(prompt).not.toContain("+123");
|
||||
expect(prompt).not.toContain("+456");
|
||||
expect(prompt).toMatch(/[a-f0-9]{12}/);
|
||||
});
|
||||
|
||||
it("uses a stable, keyed HMAC when ownerDisplaySecret is provided", () => {
|
||||
const secretA = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
ownerNumbers: ["+123"],
|
||||
ownerDisplay: "hash",
|
||||
ownerDisplaySecret: "secret-key-A",
|
||||
});
|
||||
|
||||
const secretB = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
ownerNumbers: ["+123"],
|
||||
ownerDisplay: "hash",
|
||||
ownerDisplaySecret: "secret-key-B",
|
||||
});
|
||||
|
||||
const lineA = secretA.split("## Authorized Senders")[1]?.split("\n")[1];
|
||||
const lineB = secretB.split("## Authorized Senders")[1]?.split("\n")[1];
|
||||
const tokenA = lineA?.match(/[a-f0-9]{12}/)?.[0];
|
||||
const tokenB = lineB?.match(/[a-f0-9]{12}/)?.[0];
|
||||
|
||||
expect(tokenA).toBeDefined();
|
||||
expect(tokenB).toBeDefined();
|
||||
expect(tokenA).not.toBe(tokenB);
|
||||
});
|
||||
|
||||
it("omits owner section when numbers are missing", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
|
||||
@ -5,6 +5,7 @@ import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
||||
import type { ResolvedTimeFormat } from "./date-time.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
||||
import { createHmac, createHash } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Controls which hardcoded sections are included in the system prompt.
|
||||
@ -13,6 +14,7 @@ import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
||||
* - "none": Just basic identity line, no sections
|
||||
*/
|
||||
export type PromptMode = "full" | "minimal" | "none";
|
||||
type OwnerIdDisplay = "raw" | "hash";
|
||||
|
||||
function buildSkillsSection(params: {
|
||||
skillsPrompt?: string;
|
||||
@ -73,6 +75,30 @@ function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: bool
|
||||
return ["## Authorized Senders", ownerLine, ""];
|
||||
}
|
||||
|
||||
function formatOwnerDisplayId(ownerId: string, ownerDisplaySecret?: string) {
|
||||
const hasSecret = ownerDisplaySecret?.trim();
|
||||
const digest = hasSecret
|
||||
? createHmac("sha256", hasSecret).update(ownerId).digest("hex")
|
||||
: createHash("sha256").update(ownerId).digest("hex");
|
||||
return digest.slice(0, 16);
|
||||
}
|
||||
|
||||
function buildOwnerIdentityLine(
|
||||
ownerNumbers: string[],
|
||||
ownerDisplay: OwnerIdDisplay,
|
||||
ownerDisplaySecret?: string,
|
||||
) {
|
||||
const normalized = ownerNumbers.map((value) => value.trim()).filter(Boolean);
|
||||
if (normalized.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const displayOwnerNumbers =
|
||||
ownerDisplay === "hash"
|
||||
? normalized.map((ownerId) => formatOwnerDisplayId(ownerId, ownerDisplaySecret))
|
||||
: normalized;
|
||||
return `Authorized senders: ${displayOwnerNumbers.join(", ")}. These senders are allowlisted; do not assume they are the owner.`;
|
||||
}
|
||||
|
||||
function buildTimeSection(params: { userTimezone?: string }) {
|
||||
if (!params.userTimezone) {
|
||||
return [];
|
||||
@ -172,6 +198,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
extraSystemPrompt?: string;
|
||||
ownerNumbers?: string[];
|
||||
ownerDisplay?: OwnerIdDisplay;
|
||||
ownerDisplaySecret?: string;
|
||||
reasoningTagHint?: boolean;
|
||||
toolNames?: string[];
|
||||
toolSummaries?: Record<string, string>;
|
||||
@ -322,11 +350,12 @@ export function buildAgentSystemPrompt(params: {
|
||||
const execToolName = resolveToolName("exec");
|
||||
const processToolName = resolveToolName("process");
|
||||
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
||||
const ownerNumbers = (params.ownerNumbers ?? []).map((value) => value.trim()).filter(Boolean);
|
||||
const ownerLine =
|
||||
ownerNumbers.length > 0
|
||||
? `Authorized senders: ${ownerNumbers.join(", ")}. These senders are allowlisted; do not assume they are the owner.`
|
||||
: undefined;
|
||||
const ownerDisplay = params.ownerDisplay === "hash" ? "hash" : "raw";
|
||||
const ownerLine = buildOwnerIdentityLine(
|
||||
params.ownerNumbers ?? [],
|
||||
ownerDisplay,
|
||||
params.ownerDisplaySecret,
|
||||
);
|
||||
const reasoningHint = params.reasoningTagHint
|
||||
? [
|
||||
"ALL internal reasoning MUST be inside <think>...</think>.",
|
||||
|
||||
@ -330,6 +330,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.",
|
||||
"commands.ownerAllowFrom":
|
||||
"Explicit owner allowlist for owner-only tools/commands. Use channel-native IDs (optionally prefixed like \"whatsapp:+15551234567\"). '*' is ignored.",
|
||||
"commands.ownerDisplay":
|
||||
"Controls how owner IDs are rendered in the system prompt. Allowed values: raw, hash. Default: raw.",
|
||||
"commands.ownerDisplaySecret":
|
||||
"Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.",
|
||||
"session.dmScope":
|
||||
'DM session scoping: "main" keeps continuity; "per-peer", "per-channel-peer", or "per-account-channel-peer" isolates DM history (recommended for shared inboxes/multi-account).',
|
||||
"session.identityLinks":
|
||||
|
||||
@ -224,6 +224,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"commands.restart": "Allow Restart",
|
||||
"commands.useAccessGroups": "Use Access Groups",
|
||||
"commands.ownerAllowFrom": "Command Owners",
|
||||
"commands.ownerDisplay": "Owner ID Display",
|
||||
"commands.ownerDisplaySecret": "Owner ID Hash Secret",
|
||||
"ui.seamColor": "Accent Color",
|
||||
"ui.assistant.name": "Assistant Name",
|
||||
"ui.assistant.avatar": "Assistant Avatar",
|
||||
|
||||
@ -125,6 +125,8 @@ export type MessagesConfig = {
|
||||
|
||||
export type NativeCommandsSetting = boolean | "auto";
|
||||
|
||||
export type CommandOwnerDisplay = "raw" | "hash";
|
||||
|
||||
/**
|
||||
* Per-provider allowlist for command authorization.
|
||||
* Keys are channel IDs (e.g., "discord", "whatsapp") or "*" for global default.
|
||||
@ -153,6 +155,10 @@ export type CommandsConfig = {
|
||||
useAccessGroups?: boolean;
|
||||
/** Explicit owner allowlist for owner-only tools/commands (channel-native IDs). */
|
||||
ownerAllowFrom?: Array<string | number>;
|
||||
/** How owner IDs are rendered in system prompts. */
|
||||
ownerDisplay?: CommandOwnerDisplay;
|
||||
/** Secret used to key owner ID hashes when ownerDisplay is "hash". */
|
||||
ownerDisplaySecret?: string;
|
||||
/**
|
||||
* Per-provider allowlist restricting who can use slash commands.
|
||||
* If set, overrides the channel's allowFrom for command authorization.
|
||||
|
||||
@ -3,6 +3,7 @@ import { parseByteSize } from "../cli/parse-bytes.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import { ElevatedAllowFromSchema } from "./zod-schema.agent-runtime.js";
|
||||
import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
import {
|
||||
GroupChatSchema,
|
||||
InboundDebounceSchema,
|
||||
@ -161,6 +162,8 @@ export const CommandsSchema = z
|
||||
restart: z.boolean().optional().default(true),
|
||||
useAccessGroups: z.boolean().optional(),
|
||||
ownerAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
ownerDisplay: z.enum(["raw", "hash"]).optional().default("raw"),
|
||||
ownerDisplaySecret: z.string().optional().register(sensitive),
|
||||
allowFrom: ElevatedAllowFromSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user