diff --git a/src/state/memory-identity.test.ts b/src/state/memory-identity.test.ts index 0506c2d5a67c..f30dd06eceb5 100644 --- a/src/state/memory-identity.test.ts +++ b/src/state/memory-identity.test.ts @@ -12,6 +12,7 @@ import { recheckMemoryIdentityBinding, recheckMemoryIdentityBindingRecipient, resolveMemoryIdentityBindingFromAdmission, + resolveMemoryPrincipalForUserProfile, } from "./memory-identity.js"; import { closeOpenClawStateDatabaseForTest, @@ -162,6 +163,31 @@ describe("memory identity binding", () => { ).toEqual({ kind: "unbound" }); }); + it("resolves only the active memory principal for the current Gateway profile head", () => { + const { env, profileId } = fixture(); + expect( + resolveMemoryPrincipalForUserProfile({ userProfileId: profileId, options: { env } }), + ).toBeUndefined(); + + const binding = adminLinkAdmittedMemoryIdentity({ + admission: admitted("telegram", "default", "sender-profile-principal"), + authenticatedOperatorProfileId: profileId, + authenticatedOperatorScopes: ["operator.admin"], + options: { env }, + }); + + expect( + resolveMemoryPrincipalForUserProfile({ userProfileId: profileId, options: { env } }), + ).toEqual({ + principalId: binding.principalId, + kind: "user", + revision: expect.any(String), + }); + expect( + resolveMemoryPrincipalForUserProfile({ userProfileId: "unknown-profile", options: { env } }), + ).toBeUndefined(); + }); + it("fails closed when a damaged shared database contains conflicting active bindings", () => { const { env, profileId } = fixture(); const binding = adminLinkAdmittedMemoryIdentity({ diff --git a/src/state/memory-identity.ts b/src/state/memory-identity.ts index f8eeae690cec..dacaa4a2d9f5 100644 --- a/src/state/memory-identity.ts +++ b/src/state/memory-identity.ts @@ -266,6 +266,36 @@ export function recheckMemoryOperationalPrincipal(params: { return row ? toPrincipal(row) : undefined; } +/** + * Resolves the Gateway-authenticated profile to its active memory principal. + * Control-plane callers must derive this here instead of accepting a principal + * identifier from RPC or CLI input. + */ +export function resolveMemoryPrincipalForUserProfile(params: { + userProfileId: string; + options?: OpenClawStateDatabaseOptions; +}): MemoryPrincipal | undefined { + const options = params.options ?? {}; + const userProfileId = resolveUserProfileId( + requireText(params.userProfileId, "userProfileId"), + options, + ); + if (!userProfileId) { + return undefined; + } + ensureMemoryIdentitySchema(options); + const row = openOpenClawStateDatabase(options) + .db.prepare( + `SELECT principal_id, principal_kind, revision + FROM memory_principals + WHERE user_profile_id = ? AND principal_kind = 'user' AND state = 'active'`, + ) + .get(userProfileId) as + | { principal_id: string; principal_kind: string; revision: string } + | undefined; + return row ? toPrincipal(row) : undefined; +} + /** * The only binding writer. Pairing admission supplies a one-use opaque proof; * a separately authenticated Gateway profile supplies the operator identity.