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:
Vincent Koc
2026-02-21 03:13:56 -05:00
committed by GitHub
parent fe609c0c77
commit 9abab6a2c9
10 changed files with 107 additions and 5 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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",

View File

@@ -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>.",