feat(memory): bind postbox deposits to verified turns

This commit is contained in:
Galin Iliev
2026-08-14 05:23:42 -07:00
parent 418403280b
commit 73fd65c2df
24 changed files with 721 additions and 78 deletions
+1
View File
@@ -369,6 +369,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| --- | --- |
| `plugin-sdk/memory-authorization` | Versioned serializable memory-authorization contracts and selected-capability declarations. This contract alone does not enable isolation, capability admission, or an authorization mode. |
| `plugin-sdk/memory-authorization-conformance` | Pure backend conformance helpers for the memory-authorization contract. |
| `plugin-sdk/memory-postbox-runtime` | Private-local runtime resolver and scoped binding keys for core-issued, turn-bound memory postbox source capabilities. |
| `plugin-sdk/memory-core-host-embedding-registry` | Private-local after July 2026; Lightweight memory embedding provider registry helpers |
| `plugin-sdk/memory-core-host-engine-curated` | Private-local focused curated-memory annotation parsing for doctor and promotion paths |
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine exports |
+31
View File
@@ -3,6 +3,10 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { OpenClawPluginApi, OpenClawPluginCommandDefinition } from "openclaw/plugin-sdk/core";
import type { MemoryPluginRuntime } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core";
import {
MEMORY_POSTBOX_RUN_ID_BINDING,
MEMORY_POSTBOX_TURN_CAPABILITY_BINDING,
} from "openclaw/plugin-sdk/memory-postbox-runtime";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MEMORY_CORE_AUTHORIZATION_CAPABILITIES } from "./src/authorization.js";
@@ -289,6 +293,33 @@ describe("memory-core plugin runtime registration", () => {
expect(host.remember).toHaveBeenCalledWith({ content: "durable fact" });
});
it("exposes postbox deposit only with the memory-core-scoped host binding", () => {
const factories = new Map<string, (ctx: never) => unknown>();
plugin.register(
createTestPluginApi({
config: {},
runtime: hostRuntime,
registerTool(factory, options) {
for (const name of options?.names ?? []) {
factories.set(name, factory as (ctx: never) => unknown);
}
},
}),
);
const postbox = factories.get("memory_postbox_deposit");
expect(postbox?.({ agentId: "main", sessionKey: "session" } as never)).toBeNull();
expect(
postbox?.({
agentId: "main",
sessionKey: "session",
toolBindings: {
[MEMORY_POSTBOX_TURN_CAPABILITY_BINDING]: "opaque-turn-capability",
[MEMORY_POSTBOX_RUN_ID_BINDING]: "run-1",
},
} as never),
).toMatchObject({ name: "memory_postbox_deposit" });
});
it("keeps memory manager initialization demand-driven", () => {
plugin.register(
createTestPluginApi({
+59
View File
@@ -8,6 +8,10 @@ import {
type OpenClawConfig,
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { resolveMemoryBackendConfig } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import {
MEMORY_POSTBOX_RUN_ID_BINDING,
MEMORY_POSTBOX_TURN_CAPABILITY_BINDING,
} from "openclaw/plugin-sdk/memory-postbox-runtime";
import {
definePluginEntry,
type AnyAgentTool,
@@ -58,6 +62,9 @@ const loadStandingIntentToolModule = createLazyRuntimeModule(
const loadRuntimeProviderModule = createLazyRuntimeModule(
() => import("./src/runtime-provider.js"),
);
const loadScopedMemorySharingModule = createLazyRuntimeModule(
() => import("./src/memory/scoped-memory-sharing.js"),
);
function getToolConfig(options: MemoryToolOptions): OpenClawConfig | undefined {
return options.getConfig?.() ?? options.config;
@@ -110,6 +117,15 @@ const MemoryRememberSchema = {
additionalProperties: false,
} as const satisfies TSchema;
const MemoryPostboxDepositSchema = {
type: "object",
properties: {
content: { type: "string" },
},
required: ["content"],
additionalProperties: false,
} as const satisfies TSchema;
function createLazyMemoryTool(params: {
options: MemoryToolOptions;
label: string;
@@ -202,6 +218,45 @@ function createSubjectMemoryRememberTool(ctx: OpenClawPluginToolContext): AnyAge
};
}
/** A host-scoped turn capability makes postbox deposit a one-way, source-bound operation. */
function createMemoryPostboxDepositTool(ctx: OpenClawPluginToolContext): AnyAgentTool | null {
const turnCapability = ctx.toolBindings?.[MEMORY_POSTBOX_TURN_CAPABILITY_BINDING];
const runId = ctx.toolBindings?.[MEMORY_POSTBOX_RUN_ID_BINDING];
const agentId = ctx.agentId?.trim();
const sessionKey = ctx.sessionKey?.trim();
if (typeof turnCapability !== "string" || typeof runId !== "string" || !agentId || !sessionKey) {
return null;
}
return {
label: "Memory Postbox Deposit",
name: "memory_postbox_deposit",
description:
"Submit a current-turn observation to the authorized memory postbox. Provide only the observation. The result only confirms acceptance; it cannot list, read, or identify postbox items.",
parameters: MemoryPostboxDepositSchema,
execute: async (_toolCallId, params) => {
const content =
params && typeof params === "object" && "content" in params
? (params as { content?: unknown }).content
: undefined;
if (typeof content !== "string" || !content.trim()) {
return jsonResult({ accepted: false });
}
const { depositBuiltinMemoryPostboxFromTurnCapability } =
await loadScopedMemorySharingModule();
return jsonResult(
depositBuiltinMemoryPostboxFromTurnCapability({
agentId,
runId,
sessionKey,
...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
turnCapability,
content,
}),
);
},
};
}
function createLazyStandingIntentTool(
ctx: OpenClawPluginToolContext,
reportUnavailable: (reason: string) => void,
@@ -388,6 +443,10 @@ export default definePluginEntry({
{ names: ["intent"] },
);
api.registerTool((ctx) => createMemoryPostboxDepositTool(ctx), {
names: ["memory_postbox_deposit"],
});
api.on("before_prompt_build", async (event, ctx) => {
if (ctx.memoryReadEnforced) {
return undefined;
+1 -1
View File
@@ -16,7 +16,7 @@
},
"kind": "memory",
"contracts": {
"tools": ["intent", "memory_get", "memory_search"]
"tools": ["intent", "memory_get", "memory_postbox_deposit", "memory_search"]
},
"toolMetadata": {
"intent": {
@@ -271,6 +271,7 @@ export type MemoryProjectionRow = {
export type MemoryPostboxSettingRow = {
agent_id: string;
target_store_id: string;
mode: "off" | "review-required";
updated_by_principal_id: string;
updated_at: number;
@@ -3,6 +3,10 @@ import os from "node:os";
import path from "node:path";
import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
mintMemoryPostboxTurnCapability,
resetMemoryPostboxTurnCapabilitiesForTest,
} from "../../../../src/gateway/memory-postbox-turn-capability.js";
import { openOpenClawAgentDatabase } from "../../../../src/state/openclaw-agent-db.js";
import {
createBuiltinScopedMemoryResource,
@@ -11,9 +15,9 @@ import {
import {
createBuiltinMemoryProjection,
depositBuiltinMemoryPostbox,
depositBuiltinMemoryPostboxFromTurnCapability,
expireBuiltinMemoryProjections,
refreshBuiltinMemoryProjection,
issueBuiltinMemoryPostboxSourceHandle,
registerBuiltinMemoryProjectionTarget,
revokeBuiltinMemoryProjection,
reviewBuiltinMemoryPostboxItem,
@@ -34,6 +38,7 @@ describe("builtin scoped memory sharing", () => {
afterEach(() => {
closeOpenClawAgentDatabasesForTest();
resetMemoryPostboxTurnCapabilitiesForTest();
vi.unstubAllEnvs();
fs.rmSync(stateDir, { recursive: true, force: true });
});
@@ -106,6 +111,29 @@ describe("builtin scoped memory sharing", () => {
});
}
function depositFromVerifiedTurn(params: { content: string; sourceMessageRef: string }) {
const runId = `run-${params.sourceMessageRef}`;
const sessionKey = "agent:main:direct:alice";
const token = mintMemoryPostboxTurnCapability({
agentId,
runId,
sessionKey,
sessionId: "session-alice",
sourceChannelRef: "telegram:alice",
sourceMessageRef: params.sourceMessageRef,
senderEvidenceRef: "telegram:sender-alice",
targetPrincipalId: alice,
});
return depositBuiltinMemoryPostboxFromTurnCapability({
agentId,
runId,
sessionKey,
sessionId: "session-alice",
turnCapability: token,
content: params.content,
});
}
it("creates a reviewed target copy with source lineage and no private read grant", () => {
const source = sourceStore();
const target = projectionStore();
@@ -152,7 +180,9 @@ describe("builtin scoped memory sharing", () => {
const database = openOpenClawAgentDatabase({ agentId });
expect(
database.db
.prepare("SELECT source_revision_id, reviewed_by_principal_id, expiry_kind FROM memory_projections")
.prepare(
"SELECT source_revision_id, reviewed_by_principal_id, expiry_kind FROM memory_projections",
)
.get(),
).toEqual({
source_revision_id: privateRevision.revisionId,
@@ -350,7 +380,9 @@ describe("builtin scoped memory sharing", () => {
}),
).toBeUndefined();
expect(
database.db.prepare("SELECT state FROM memory_projections WHERE projection_id = ?").get(projection.projectionId),
database.db
.prepare("SELECT state FROM memory_projections WHERE projection_id = ?")
.get(projection.projectionId),
).toEqual({ state: "expired" });
});
@@ -422,15 +454,9 @@ describe("builtin scoped memory sharing", () => {
it("keeps postbox off by default and makes review-required deposits one-way", () => {
const target = privateTargetStore();
const offHandle = issueBuiltinMemoryPostboxSourceHandle({
agentId,
sourceSessionId: "group-session",
sourceChannelRef: "channel-ref",
sourceMessageRef: "message-ref-off",
senderEvidenceRef: "sender-proof",
targetStoreId: target.storeId,
});
expect(depositBuiltinMemoryPostbox({ agentId, sourceHandleId: offHandle, content: "observation" })).toEqual({ accepted: false });
expect(
depositFromVerifiedTurn({ content: "observation", sourceMessageRef: "message-ref-off" }),
).toEqual({ accepted: false });
setBuiltinMemoryPostboxMode({
agentId,
@@ -438,30 +464,34 @@ describe("builtin scoped memory sharing", () => {
operatorPrincipalId: alice,
mode: "review-required",
});
const handle = issueBuiltinMemoryPostboxSourceHandle({
agentId,
sourceSessionId: "group-session",
sourceChannelRef: "channel-ref",
sourceMessageRef: "message-ref-live",
senderEvidenceRef: "sender-proof",
targetStoreId: target.storeId,
});
expect(depositBuiltinMemoryPostbox({ agentId, sourceHandleId: "forged", content: "observation" })).toEqual({ accepted: false });
expect(depositBuiltinMemoryPostbox({ agentId, sourceHandleId: handle, content: "observation" })).toEqual({ accepted: true });
expect(depositBuiltinMemoryPostbox({ agentId, sourceHandleId: handle, content: "observation" })).toEqual({ accepted: false });
expect(
depositBuiltinMemoryPostbox({ agentId, sourceHandleId: "forged", content: "observation" }),
).toEqual({ accepted: false });
expect(
depositFromVerifiedTurn({ content: "observation", sourceMessageRef: "message-ref-live" }),
).toEqual({ accepted: true });
const database = openOpenClawAgentDatabase({ agentId });
const item = database.db.prepare("SELECT item_id, state FROM memory_postbox_items").get() as { item_id: string; state: string };
const item = database.db.prepare("SELECT item_id, state FROM memory_postbox_items").get() as {
item_id: string;
state: string;
};
expect(item.state).toBe("postbox");
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_resources").get()).toEqual({ count: 0 });
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_resources").get()).toEqual({
count: 0,
});
reviewBuiltinMemoryPostboxItem({
agentId,
itemId: item.item_id,
operatorPrincipalId: alice,
decision: "approve",
});
expect(database.db.prepare("SELECT state FROM memory_postbox_items").get()).toEqual({ state: "reviewed" });
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_resources").get()).toEqual({ count: 0 });
expect(database.db.prepare("SELECT state FROM memory_postbox_items").get()).toEqual({
state: "reviewed",
});
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_resources").get()).toEqual({
count: 0,
});
});
it("enforces the persisted per-channel target cap without exposing deposited content", () => {
@@ -473,24 +503,23 @@ describe("builtin scoped memory sharing", () => {
mode: "review-required",
});
for (let index = 0; index < 13; index += 1) {
const handle = issueBuiltinMemoryPostboxSourceHandle({
agentId,
sourceSessionId: `source-session-${index}`,
sourceChannelRef: "channel-ref",
sourceMessageRef: `message-${index}`,
senderEvidenceRef: "sender-proof",
targetStoreId: target.storeId,
});
expect(
depositBuiltinMemoryPostbox({ agentId, sourceHandleId: handle, content: `observation-${index}` }),
depositFromVerifiedTurn({
content: `observation-${index}`,
sourceMessageRef: `message-${index}`,
}),
).toEqual({ accepted: index < 12 });
}
const database = openOpenClawAgentDatabase({ agentId });
expect(database.db.prepare("SELECT deposit_count FROM memory_postbox_rate_limits").get()).toEqual({
expect(
database.db.prepare("SELECT deposit_count FROM memory_postbox_rate_limits").get(),
).toEqual({
deposit_count: 12,
});
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_postbox_items").get()).toEqual({
count: 12,
});
expect(database.db.prepare("SELECT COUNT(*) AS count FROM memory_postbox_items").get()).toEqual(
{
count: 12,
},
);
});
});
@@ -1,27 +1,25 @@
import { createHash, randomUUID } from "node:crypto";
import type { AudienceRef } from "openclaw/plugin-sdk/memory-authorization";
import { resolveMemoryPostboxTurnCapability } from "openclaw/plugin-sdk/memory-postbox-runtime";
import {
executeSqliteQuerySync,
executeSqliteQueryTakeFirstSync,
getNodeSqliteKysely,
runSqliteImmediateTransactionSync,
} from "openclaw/plugin-sdk/sqlite-runtime";
import type { AudienceRef } from "openclaw/plugin-sdk/memory-authorization";
import { type ScopedMemoryDatabase, withScopedMemoryDatabase } from "./scoped-memory-db.js";
import { evaluateBuiltinScopedMemoryPolicy } from "./scoped-memory-policy.js";
import {
createBuiltinScopedMemoryResource,
readBuiltinScopedMemoryRevisionSnapshot,
type BuiltinScopedMemoryDerivedSource,
type BuiltinScopedMemoryRevision,
} from "./scoped-memory-resources.js";
import { evaluateBuiltinScopedMemoryPolicy } from "./scoped-memory-policy.js";
import {
createScopedMemorySourcePolicySetId,
normalizeScopedMemoryRequiredText,
type BuiltinScopedMemoryStore,
} from "./scoped-memory-store.js";
import {
type ScopedMemoryDatabase,
withScopedMemoryDatabase,
} from "./scoped-memory-db.js";
const POSTBOX_HANDLE_TTL_MS = 10 * 60 * 1000;
const POSTBOX_RATE_WINDOW_MS = 60 * 60 * 1000;
@@ -56,12 +54,11 @@ function normalize(value: string, label: string): string {
return normalizeScopedMemoryRequiredText(value, label);
}
function targetAudience(params: { kind: ProjectionAudience["kind"]; id: string }): ProjectionAudience {
if (
params.kind !== "conversation" &&
params.kind !== "role" &&
params.kind !== "agent-shared"
) {
function targetAudience(params: {
kind: ProjectionAudience["kind"];
id: string;
}): ProjectionAudience {
if (params.kind !== "conversation" && params.kind !== "role" && params.kind !== "agent-shared") {
throw new Error("memory projection target is unavailable");
}
return Object.freeze({ kind: params.kind, id: normalize(params.id, "projection target id") });
@@ -115,6 +112,25 @@ function readTargetStore(params: {
);
}
/** The private postbox target is selected from core-verified source identity, never tool input. */
function readPostboxTargetStore(params: {
database: Parameters<typeof getNodeSqliteKysely<ScopedMemoryDatabase>>[0];
agentId: string;
targetPrincipalId: string;
}) {
const db = getNodeSqliteKysely<ScopedMemoryDatabase>(params.database);
return executeSqliteQueryTakeFirstSync(
params.database,
db
.selectFrom("memory_stores")
.select(["store_id", "audience_kind", "audience_id"])
.where("agent_id", "=", params.agentId)
.where("audience_kind", "=", "user")
.where("audience_id", "=", params.targetPrincipalId)
.where("lifecycle_state", "=", "active"),
);
}
function readSource(params: {
database: Parameters<typeof getNodeSqliteKysely<ScopedMemoryDatabase>>[0];
agentId: string;
@@ -159,7 +175,10 @@ function readSourceRequirements(params: {
if (policyRequirements.length === 0) {
throw new Error("memory projection source is unavailable");
}
return Object.freeze({ revisionId: params.revisionId, policyRequirements: Object.freeze(policyRequirements) });
return Object.freeze({
revisionId: params.revisionId,
policyRequirements: Object.freeze(policyRequirements),
});
}
/**
@@ -195,11 +214,7 @@ export function registerBuiltinMemoryProjectionTarget(params: {
.where("agent_id", "=", params.agentId)
.where("lifecycle_state", "=", "active"),
);
if (
!store ||
store.audience_kind !== target.kind ||
store.audience_id !== target.id
) {
if (!store || store.audience_kind !== target.kind || store.audience_id !== target.id) {
throw new Error("memory projection target is unavailable");
}
executeSqliteQuerySync(
@@ -236,7 +251,9 @@ export function createBuiltinMemoryProjection(params: {
purpose: string;
preview: string;
content: string;
expiry: Readonly<{ kind: "expires"; expiresAt: number }> | Readonly<{ kind: "no-expiry"; auditReason: string }>;
expiry:
| Readonly<{ kind: "expires"; expiresAt: number }>
| Readonly<{ kind: "no-expiry"; auditReason: string }>;
nowMs?: number;
}): BuiltinMemoryProjection {
const target = targetAudience(params.target);
@@ -375,7 +392,9 @@ function tombstoneProjectionCopy(params: {
);
executeSqliteQuerySync(
params.database,
db.deleteFrom("memory_scoped_chunks").where("revision_id", "=", params.projection.copy_revision_id),
db
.deleteFrom("memory_scoped_chunks")
.where("revision_id", "=", params.projection.copy_revision_id),
);
executeSqliteQuerySync(
params.database,
@@ -498,7 +517,9 @@ export function refreshBuiltinMemoryProjection(params: {
purpose: string;
preview: string;
content: string;
expiry: Readonly<{ kind: "expires"; expiresAt: number }> | Readonly<{ kind: "no-expiry"; auditReason: string }>;
expiry:
| Readonly<{ kind: "expires"; expiresAt: number }>
| Readonly<{ kind: "no-expiry"; auditReason: string }>;
nowMs?: number;
}): Readonly<{ projection: BuiltinMemoryProjection; replacedExposureSetIds: readonly string[] }> {
const projectionId = normalize(params.projectionId, "projection id");
@@ -587,12 +608,13 @@ export function setBuiltinMemoryPostboxMode(params: {
.insertInto("memory_postbox_settings")
.values({
agent_id: params.agentId,
target_store_id: params.targetStoreId,
mode: params.mode,
updated_by_principal_id: normalize(params.operatorPrincipalId, "operator principal id"),
updated_at: nowMs,
})
.onConflict((conflict) =>
conflict.column("agent_id").doUpdateSet({
conflict.columns(["agent_id", "target_store_id"]).doUpdateSet({
mode: params.mode,
updated_by_principal_id: normalize(params.operatorPrincipalId, "operator principal id"),
updated_at: nowMs,
@@ -603,7 +625,7 @@ export function setBuiltinMemoryPostboxMode(params: {
}
/** Only core/channel ingress may mint this opaque handle from current verified sender evidence. */
export function issueBuiltinMemoryPostboxSourceHandle(params: {
function issueBuiltinMemoryPostboxSourceHandle(params: {
agentId: string;
sourceSessionId: string;
sourceChannelRef: string;
@@ -647,6 +669,62 @@ export function issueBuiltinMemoryPostboxSourceHandle(params: {
});
}
/**
* Redeems the core-issued turn token before minting the persisted one-use handle. The selected
* memory plugin resolves the core-selected private subject to its quarantine store; tool input cannot.
*/
export function depositBuiltinMemoryPostboxFromTurnCapability(params: {
agentId: string;
runId: string;
sessionKey: string;
sessionId?: string;
turnCapability: string;
content: string;
nowMs?: number;
}): BuiltinMemoryPostboxDeposit {
const source = resolveMemoryPostboxTurnCapability({
token: params.turnCapability,
agentId: params.agentId,
runId: params.runId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
nowMs: params.nowMs,
});
if (!source) {
return Object.freeze({ accepted: false });
}
try {
const targetStoreId = withScopedMemoryDatabase(params.agentId, (database) => {
const target = readPostboxTargetStore({
database,
agentId: params.agentId,
targetPrincipalId: source.targetPrincipalId,
});
return target?.store_id;
});
if (!targetStoreId) {
return Object.freeze({ accepted: false });
}
const sourceHandleId = issueBuiltinMemoryPostboxSourceHandle({
agentId: params.agentId,
sourceSessionId: params.sessionId ?? params.sessionKey,
sourceChannelRef: source.sourceChannelRef,
sourceMessageRef: source.sourceMessageRef,
senderEvidenceRef: source.senderEvidenceRef,
targetStoreId,
nowMs: params.nowMs,
});
return depositBuiltinMemoryPostbox({
agentId: params.agentId,
sourceHandleId,
content: params.content,
nowMs: params.nowMs,
});
} catch {
return Object.freeze({ accepted: false });
}
}
/** The source can only receive accepted/generic-refusal; there is no read, list, or exact-get continuation. */
export function depositBuiltinMemoryPostbox(params: {
agentId: string;
@@ -675,7 +753,8 @@ export function depositBuiltinMemoryPostbox(params: {
db
.selectFrom("memory_postbox_settings")
.select("mode")
.where("agent_id", "=", params.agentId),
.where("agent_id", "=", params.agentId)
.where("target_store_id", "=", handle?.target_store_id ?? ""),
);
if (
!handle ||
@@ -771,7 +850,13 @@ export function reviewBuiltinMemoryPostboxItem(params: {
db
.selectFrom("memory_postbox_items as item")
.innerJoin("memory_stores as store", "store.store_id", "item.target_store_id")
.select(["item.item_id", "item.state", "item.target_store_id", "store.audience_kind", "store.audience_id"])
.select([
"item.item_id",
"item.state",
"item.target_store_id",
"store.audience_kind",
"store.audience_id",
])
.where("item.item_id", "=", params.itemId)
.where("item.agent_id", "=", params.agentId),
);
@@ -785,7 +870,12 @@ export function reviewBuiltinMemoryPostboxItem(params: {
audience: { kind: item.audience_kind, id: item.audience_id },
operation: "policy-admin",
});
const state = params.decision === "approve" ? "reviewed" : params.decision === "reject" ? "rejected" : "purged";
const state =
params.decision === "approve"
? "reviewed"
: params.decision === "reject"
? "rejected"
: "purged";
executeSqliteQuerySync(
database,
db
+4
View File
@@ -108,6 +108,7 @@
"!dist/plugin-sdk/llm.d.ts",
"!dist/plugin-sdk/markdown-table-runtime.d.ts",
"!dist/plugin-sdk/media-generation-runtime.d.ts",
"!dist/plugin-sdk/memory-postbox-runtime.d.ts",
"!dist/plugin-sdk/memory-core-host-embedding-registry.d.ts",
"!dist/plugin-sdk/memory-core-host-engine-curated.d.ts",
"!dist/plugin-sdk/memory-core-host-engine-embeddings.d.ts",
@@ -1214,6 +1215,9 @@
"types": "./dist/plugin-sdk/memory-authorization-conformance.d.ts",
"default": "./dist/plugin-sdk/memory-authorization-conformance.js"
},
"./plugin-sdk/memory-postbox-runtime": {
"default": "./dist/plugin-sdk/memory-postbox-runtime.js"
},
"./plugin-sdk/memory-core-host-embedding-registry": {
"default": "./dist/plugin-sdk/memory-core-host-embedding-registry.js"
},
+1
View File
@@ -254,6 +254,7 @@
"qa-runner-runtime",
"memory-authorization",
"memory-authorization-conformance",
"memory-postbox-runtime",
"memory-core-host-embedding-registry",
"memory-core-host-engine-embeddings",
"memory-core-host-engine-curated",
@@ -65,6 +65,7 @@
"llm",
"markdown-table-runtime",
"media-generation-runtime",
"memory-postbox-runtime",
"memory-core-host-embedding-registry",
"memory-core-host-engine-curated",
"memory-core-host-engine-embeddings",
+32 -7
View File
@@ -19,6 +19,10 @@ import { resolveEventSessionRoutingPolicy } from "../infra/event-session-routing
import { applyExecPolicyLayer } from "../infra/exec-policy.js";
import { mergeGatewayAgentCliPath } from "../infra/openclaw-cli-shim.js";
import { logWarn } from "../logger.js";
import {
MEMORY_POSTBOX_RUN_ID_BINDING,
MEMORY_POSTBOX_TURN_CAPABILITY_BINDING,
} from "../plugins/memory-postbox-tool-binding.js";
import type {
PluginHookChannelContext,
PluginHookToolRequesterContext,
@@ -181,6 +185,8 @@ type OpenClawCodingToolsOptions = {
clientCaps?: string[];
/** Out-of-band plugin bindings attached by the run initiator. */
toolBindings?: Readonly<Record<string, unknown>>;
/** Host-owned bindings delivered only to their named plugin tool factory. */
pluginToolBindings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
/** Trusted runtime-only authorization for one bounded cross-conversation recall pass. */
conversationRecall?: ConversationRecallContext;
/** Normalized conversation kind when the caller already has channel metadata. */
@@ -194,6 +200,8 @@ type OpenClawCodingToolsOptions = {
nativeChannelId?: string;
/** Opaque host-issued capability for current-turn channel message actions. */
messageActionTurnCapability?: string;
/** Opaque core-issued source-message capability for one postbox deposit. */
memoryPostboxTurnCapability?: string;
sandbox?: SandboxContext | null;
sessionKey?: string;
/**
@@ -394,6 +402,21 @@ type OpenClawCodingToolsOptions = {
scheduledToolPolicy?: ScheduledToolPolicyContext;
};
function resolvePluginToolBindings(options: OpenClawCodingToolsOptions | undefined) {
const turnCapability = options?.memoryPostboxTurnCapability?.trim();
const runId = options?.runId?.trim();
if (!turnCapability || !runId) {
return options?.pluginToolBindings;
}
return {
...options?.pluginToolBindings,
"memory-core": {
[MEMORY_POSTBOX_TURN_CAPABILITY_BINDING]: turnCapability,
[MEMORY_POSTBOX_RUN_ID_BINDING]: runId,
},
} satisfies Readonly<Record<string, Readonly<Record<string, unknown>>>>;
}
function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions): AnyAgentTool[] {
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
const isMemoryFlushRun = options?.trigger === "memory";
@@ -772,6 +795,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
clientCaps: options?.clientCaps,
toolBindings: options?.toolBindings,
pluginToolBindings: resolvePluginToolBindings(options),
authProfileStore: options?.authProfileStore,
},
resolvedConfig: options?.config,
@@ -926,12 +950,11 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
toolsForMemoryFlush.push(tool);
}
}
const unavailableCoreToolReason =
isMemoryFlushRun
? memoryIsolationCutover
? "memory-triggered compaction runs expose only subject-scoped memory_remember"
: "memory-triggered compaction runs expose only read and append-only write"
: undefined;
const unavailableCoreToolReason = isMemoryFlushRun
? memoryIsolationCutover
? "memory-triggered compaction runs expose only subject-scoped memory_remember"
: "memory-triggered compaction runs expose only read and append-only write"
: undefined;
const toolsForMessageProvider = filterToolsByMessageProvider(
toolsForMemoryFlush,
options?.toolPolicyMessageProvider ?? options?.messageProvider,
@@ -994,7 +1017,9 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
? authorizedTools.filter((tool) => AUTHORIZED_MEMORY_VIEW_TOOL_NAMES.has(tool.name))
: memoryIsolationCutover
? isMemoryFlushRun
? authorizedTools.filter((tool) => AUTHORIZED_MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name))
? authorizedTools.filter((tool) =>
AUTHORIZED_MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name),
)
: authorizedTools.filter((tool) => MEMORY_ISOLATION_READ_TOOL_NAMES.has(tool.name))
: authorizedTools;
if (
@@ -9,7 +9,10 @@ import {
logCodeModeDiagnostic,
} from "../../../logging/code-mode-diagnostic.js";
import { extractModelCompat } from "../../../plugins/provider-model-compat.js";
import type { AuthorizedMemoryReadHost, AuthorizedMemoryWriteHost } from "../../../plugins/tool-types.js";
import type {
AuthorizedMemoryReadHost,
AuthorizedMemoryWriteHost,
} from "../../../plugins/tool-types.js";
import { getPluginToolMeta } from "../../../plugins/tools.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { normalizeDeliveryContext } from "../../../utils/delivery-context.shared.js";
@@ -306,6 +309,7 @@ export async function prepareEmbeddedAttemptToolBase(params: {
messageThreadId: attempt.messageThreadId,
nativeChannelId: attempt.chatId,
messageActionTurnCapability: attempt.messageActionTurnCapability,
memoryPostboxTurnCapability: attempt.memoryPostboxTurnCapability,
groupId: attempt.groupId,
groupChannel: attempt.groupChannel,
groupSpace: attempt.groupSpace,
@@ -137,6 +137,8 @@ export type RunEmbeddedAgentParams = {
memberRoleIds?: string[];
/** Opaque host-issued capability for current-turn channel message actions. */
messageActionTurnCapability?: string;
/** Opaque core-issued source-message capability for one postbox deposit. */
memoryPostboxTurnCapability?: string;
/** Parent session key for subagent policy inheritance. */
spawnedBy?: string | null;
/** Whether workspaceDir points at the canonical agent workspace for bootstrap purposes. */
@@ -286,6 +286,7 @@ export async function dispatchEmbeddedRunAttempt(input: {
messageThreadId: params.messageThreadId,
conversationToolPolicy: params.conversationToolPolicy,
messageActionTurnCapability: params.messageActionTurnCapability,
memoryPostboxTurnCapability: params.memoryPostboxTurnCapability,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
@@ -60,6 +60,7 @@ export type OpenClawPluginToolOptions = {
sandboxed?: boolean;
allowGatewaySubagentBinding?: boolean;
toolBindings?: Readonly<Record<string, unknown>>;
pluginToolBindings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
activeProjectKeys?: readonly string[];
};
@@ -137,6 +138,7 @@ export function resolveOpenClawPluginToolInputs(params: {
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
toolBindings: options?.toolBindings,
pluginToolBindings: options?.pluginToolBindings,
activeProjectKeys: options?.activeProjectKeys,
conversationRecall: options?.conversationRecall,
...(memoryReadEnforced ? { memoryReadEnforced: true as const } : {}),
@@ -20,6 +20,11 @@ import {
resolveMessageActionTurnCapabilityLifetime,
revokeMessageActionTurnCapability,
} from "../../gateway/message-action-turn-capability.js";
import {
mintMemoryPostboxTurnCapability,
resolveMemoryPostboxTargetPrincipal,
revokeMemoryPostboxTurnCapability,
} from "../../gateway/memory-postbox-turn-capability.js";
import { logVerbose } from "../../globals.js";
import {
isMarkdownCapableMessageChannel,
@@ -180,6 +185,37 @@ export async function runEmbeddedFallbackCandidate(params: {
...resolveMessageActionTurnCapabilityLifetime(runBaseParams.timeoutMs),
})
: undefined;
const memoryPostboxTargetPrincipal =
embeddedContext.agentId && messageActionCapabilitySessionKey && embeddedContext.sessionId
? resolveMemoryPostboxTargetPrincipal({
agentId: embeddedContext.agentId,
sessionKey: messageActionCapabilitySessionKey,
sessionId: embeddedContext.sessionId,
})
: undefined;
const memoryPostboxTurnCapability =
isTrustedMessageActionTurnIngress(turn.sessionCtx.Provider) &&
!turn.isHeartbeat &&
embeddedContext.agentId &&
messageActionCapabilitySessionKey &&
embeddedContext.sessionId &&
embeddedContext.messageProvider &&
embeddedContext.currentChannelId &&
embeddedContext.currentMessageId !== undefined &&
senderContext.senderId &&
memoryPostboxTargetPrincipal
? mintMemoryPostboxTurnCapability({
agentId: embeddedContext.agentId,
runId: params.runId,
sessionKey: messageActionCapabilitySessionKey,
sessionId: embeddedContext.sessionId,
sourceChannelRef: `${embeddedContext.messageProvider}:${embeddedContext.currentChannelId}`,
sourceMessageRef: String(embeddedContext.currentMessageId),
senderEvidenceRef: `${embeddedContext.messageProvider}:${senderContext.senderId}`,
targetPrincipalId: memoryPostboxTargetPrincipal,
...resolveMessageActionTurnCapabilityLifetime(runBaseParams.timeoutMs),
})
: undefined;
let attemptCompactionCount = 0;
const lifecycleBackstop = createAgentLifecycleTerminalBackstop({
runId: params.runId,
@@ -212,6 +248,7 @@ export async function runEmbeddedFallbackCandidate(params: {
preparedRunAdmission: params.preparedRunAdmission,
...embeddedContext,
messageActionTurnCapability,
memoryPostboxTurnCapability,
lifecycleGeneration: params.getLifecycleGeneration(),
allowGatewaySubagentBinding: true,
trigger: turn.isHeartbeat ? "heartbeat" : "user",
@@ -453,5 +490,6 @@ export async function runEmbeddedFallbackCandidate(params: {
} finally {
params.onCompactionCount(attemptCompactionCount);
revokeMessageActionTurnCapability(messageActionTurnCapability);
revokeMemoryPostboxTurnCapability(memoryPostboxTurnCapability);
}
}
@@ -0,0 +1,124 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const sessionContextMock = vi.hoisted(() => vi.fn());
vi.mock("../state/memory-session-subject.js", () => ({
createCurrentMemorySessionContext: sessionContextMock,
}));
import {
mintMemoryPostboxTurnCapability,
resetMemoryPostboxTurnCapabilitiesForTest,
resolveMemoryPostboxTurnCapability,
resolveMemoryPostboxTargetPrincipal,
revokeMemoryPostboxTurnCapability,
} from "./memory-postbox-turn-capability.js";
describe("memory postbox turn capability", () => {
afterEach(() => {
resetMemoryPostboxTurnCapabilitiesForTest();
sessionContextMock.mockReset();
});
it("binds trusted source evidence to one live agent run and session", () => {
const token = mintMemoryPostboxTurnCapability({
agentId: "main",
runId: "run-1",
sessionKey: "agent:main:group:team",
sessionId: "session-1",
sourceChannelRef: "telegram:team",
sourceMessageRef: "message-1",
senderEvidenceRef: "sender-evidence-1",
targetPrincipalId: "principal-alice",
nowMs: 100,
ttlMs: 1_000,
});
expect(
resolveMemoryPostboxTurnCapability({
token,
agentId: "main",
runId: "run-1",
sessionKey: "agent:main:group:team",
sessionId: "session-1",
nowMs: 101,
}),
).toEqual({
sourceChannelRef: "telegram:team",
sourceMessageRef: "message-1",
senderEvidenceRef: "sender-evidence-1",
targetPrincipalId: "principal-alice",
});
for (const mismatch of [
{ agentId: "other", runId: "run-1", sessionKey: "agent:main:group:team", sessionId: "session-1" },
{ agentId: "main", runId: "run-2", sessionKey: "agent:main:group:team", sessionId: "session-1" },
{ agentId: "main", runId: "run-1", sessionKey: "agent:main:group:other", sessionId: "session-1" },
{ agentId: "main", runId: "run-1", sessionKey: "agent:main:group:team", sessionId: "session-2" },
]) {
expect(resolveMemoryPostboxTurnCapability({ token, ...mismatch, nowMs: 101 })).toBeUndefined();
}
expect(
resolveMemoryPostboxTurnCapability({
token,
agentId: "main",
runId: "run-1",
sessionKey: "agent:main:group:team",
sessionId: "session-1",
nowMs: 1_100,
}),
).toBeUndefined();
});
it("cannot be redeemed after explicit terminal revocation", () => {
const token = mintMemoryPostboxTurnCapability({
agentId: "main",
runId: "run-1",
sessionKey: "agent:main:group:team",
sourceChannelRef: "telegram:team",
sourceMessageRef: "message-1",
senderEvidenceRef: "sender-evidence-1",
targetPrincipalId: "principal-alice",
});
expect(revokeMemoryPostboxTurnCapability(token)).toBe(true);
expect(
resolveMemoryPostboxTurnCapability({
token,
agentId: "main",
runId: "run-1",
sessionKey: "agent:main:group:team",
}),
).toBeUndefined();
});
it("selects a postbox target only from the current verified direct-user session", () => {
sessionContextMock.mockReturnValue({
kind: "current",
context: { subject: { kind: "user" }, principalId: "principal-alice" },
});
expect(
resolveMemoryPostboxTargetPrincipal({
agentId: "main",
sessionKey: "agent:main:direct:alice",
sessionId: "session-1",
}),
).toBe("principal-alice");
expect(sessionContextMock).toHaveBeenCalledWith({
sessionKey: "agent:main:direct:alice",
sessionId: "session-1",
options: { agentId: "main" },
});
sessionContextMock.mockReturnValue({
kind: "current",
context: { subject: { kind: "conversation" }, principalId: "conversation-team" },
});
expect(
resolveMemoryPostboxTargetPrincipal({
agentId: "main",
sessionKey: "agent:main:group:team",
sessionId: "session-2",
}),
).toBeUndefined();
});
});
@@ -0,0 +1,164 @@
import { randomBytes } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { createCurrentMemorySessionContext } from "../state/memory-session-subject.js";
const DEFAULT_TTL_MS = 15 * 60_000;
const MAX_TTL_MS = 24 * 60 * 60_000;
const MAX_ACTIVE_CAPABILITIES = 4096;
const RUN_LIFETIME_EXPIRES_AT_MS = Number.MAX_SAFE_INTEGER;
type MemoryPostboxTurnCapability = Readonly<{
agentId: string;
runId: string;
sessionKey: string;
sessionId?: string;
sourceChannelRef: string;
sourceMessageRef: string;
senderEvidenceRef: string;
targetPrincipalId: string;
expiresAtMs: number;
}>;
export type ResolvedMemoryPostboxSource = Readonly<{
sourceChannelRef: string;
sourceMessageRef: string;
senderEvidenceRef: string;
/** The core-selected private subject whose quarantine receives this source. */
targetPrincipalId: string;
}>;
const capabilitiesByToken = new Map<string, MemoryPostboxTurnCapability>();
function resolveTtlMs(value: number | undefined): number {
if (!Number.isFinite(value) || value === undefined || value <= 0) {
return DEFAULT_TTL_MS;
}
return Math.min(Math.trunc(value), MAX_TTL_MS);
}
function required(value: string | undefined, label: string): string {
const normalized = normalizeOptionalString(value);
if (!normalized) {
throw new Error(`memory postbox turn capability requires ${label}`);
}
return normalized;
}
function sweepExpiredMemoryPostboxTurnCapabilities(nowMs: number = Date.now()): void {
for (const [token, capability] of capabilitiesByToken) {
if (nowMs >= capability.expiresAtMs) {
capabilitiesByToken.delete(token);
}
}
}
/**
* Minted only from an admitted inbound user turn. The opaque token is later scoped to the selected
* memory plugin, which redeems current channel/sender evidence without accepting it from tool args.
*/
export function mintMemoryPostboxTurnCapability(params: {
agentId: string;
runId: string;
sessionKey: string;
sessionId?: string;
sourceChannelRef: string;
sourceMessageRef: string;
senderEvidenceRef: string;
/** Resolved only by core from the current verified user session subject. */
targetPrincipalId: string;
expiresWithRun?: boolean;
ttlMs?: number;
nowMs?: number;
}): string {
const agentId = normalizeAgentId(params.agentId);
const runId = required(params.runId, "run identity");
const sessionKey = required(params.sessionKey, "session identity");
const nowMs = params.nowMs ?? Date.now();
sweepExpiredMemoryPostboxTurnCapabilities(nowMs);
pruneMapToMaxSize(capabilitiesByToken, MAX_ACTIVE_CAPABILITIES - 1);
const token = randomBytes(32).toString("base64url");
capabilitiesByToken.set(
token,
Object.freeze({
agentId,
runId,
sessionKey,
sessionId: normalizeOptionalString(params.sessionId),
sourceChannelRef: required(params.sourceChannelRef, "source channel evidence"),
sourceMessageRef: required(params.sourceMessageRef, "source message evidence"),
senderEvidenceRef: required(params.senderEvidenceRef, "sender evidence"),
targetPrincipalId: required(params.targetPrincipalId, "target principal"),
expiresAtMs: params.expiresWithRun
? RUN_LIFETIME_EXPIRES_AT_MS
: nowMs + resolveTtlMs(params.ttlMs),
}),
);
return token;
}
/** Rejects forged, stale, cross-agent, cross-run, and cross-session tokens without disclosure. */
export function resolveMemoryPostboxTurnCapability(params: {
token?: string;
agentId: string;
runId?: string;
sessionKey: string;
sessionId?: string;
nowMs?: number;
}): ResolvedMemoryPostboxSource | undefined {
const token = normalizeOptionalString(params.token);
if (!token) {
return undefined;
}
const capability = capabilitiesByToken.get(token);
if (!capability) {
return undefined;
}
const nowMs = params.nowMs ?? Date.now();
if (
nowMs >= capability.expiresAtMs ||
capability.agentId !== normalizeAgentId(params.agentId) ||
capability.runId !== normalizeOptionalString(params.runId) ||
capability.sessionKey !== normalizeOptionalString(params.sessionKey) ||
(capability.sessionId && capability.sessionId !== normalizeOptionalString(params.sessionId))
) {
if (nowMs >= capability.expiresAtMs) {
capabilitiesByToken.delete(token);
}
return undefined;
}
return Object.freeze({
sourceChannelRef: capability.sourceChannelRef,
sourceMessageRef: capability.sourceMessageRef,
senderEvidenceRef: capability.senderEvidenceRef,
targetPrincipalId: capability.targetPrincipalId,
});
}
/**
* A postbox source can target only the current verified user's quarantine. Group and autonomous
* sessions intentionally have no fallback target: a sender id or model argument must not pick one.
*/
export function resolveMemoryPostboxTargetPrincipal(params: {
agentId: string;
sessionKey: string;
sessionId: string;
}): string | undefined {
const session = createCurrentMemorySessionContext({
sessionKey: params.sessionKey,
sessionId: params.sessionId,
options: { agentId: params.agentId },
});
return session.kind === "current" && session.context.subject.kind === "user"
? session.context.principalId
: undefined;
}
export function revokeMemoryPostboxTurnCapability(token: string | undefined): boolean {
return Boolean(token && capabilitiesByToken.delete(token));
}
export function resetMemoryPostboxTurnCapabilitiesForTest(): void {
capabilitiesByToken.clear();
}
+9
View File
@@ -0,0 +1,9 @@
/** Core-issued source-message capability for selected memory plugins. */
export {
MEMORY_POSTBOX_RUN_ID_BINDING,
MEMORY_POSTBOX_TURN_CAPABILITY_BINDING,
} from "../plugins/memory-postbox-tool-binding.js";
export {
resolveMemoryPostboxTurnCapability,
type ResolvedMemoryPostboxSource,
} from "../gateway/memory-postbox-turn-capability.js";
@@ -0,0 +1,3 @@
/** Scoped host-binding keys; only the selected memory plugin receives these values. */
export const MEMORY_POSTBOX_TURN_CAPABILITY_BINDING = "memoryPostboxTurnCapability";
export const MEMORY_POSTBOX_RUN_ID_BINDING = "memoryPostboxRunId";
+2
View File
@@ -81,6 +81,8 @@ export type OpenClawPluginToolContext = {
sessionId?: string;
/** Out-of-band plugin-owned bindings attached by the current run initiator. */
toolBindings?: Readonly<Record<string, unknown>>;
/** Host-owned bindings scoped to one selected plugin before its factory runs. */
pluginToolBindings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
/** Host-prepared repository identities for project-aware tool behavior. */
activeProjectKeys?: readonly string[];
/** Trusted runtime-only authorization for one bounded cross-conversation recall pass. */
+43
View File
@@ -2020,6 +2020,49 @@ describe("resolvePluginTools optional tools", () => {
expect(registry.diagnostics).toHaveLength(0);
});
it("delivers a host binding only to its selected plugin factory", () => {
const memoryFactory = vi.fn(() => makeTool("memory_postbox_deposit"));
const otherFactory = vi.fn(() => makeTool("other_tool"));
setRegistry([
{
pluginId: "memory-core",
optional: false,
source: "/tmp/memory-core.js",
names: ["memory_postbox_deposit"],
factory: memoryFactory,
},
{
pluginId: "other-plugin",
optional: false,
source: "/tmp/other-plugin.js",
names: ["other_tool"],
factory: otherFactory,
},
]);
const tools = resolvePluginTools(
createResolveToolsParams({
context: {
...createContext(),
toolBindings: { ordinaryBinding: "ordinary" },
pluginToolBindings: {
"memory-core": { memoryPostboxTurnCapability: "opaque" },
},
},
}),
);
expectResolvedToolNames(tools, ["memory_postbox_deposit", "other_tool"]);
expect(memoryFactory).toHaveBeenCalledWith(
expect.objectContaining({ toolBindings: { memoryPostboxTurnCapability: "opaque" } }),
);
expect(memoryFactory.mock.calls[0]?.[0]).not.toHaveProperty("pluginToolBindings");
expect(otherFactory).toHaveBeenCalledWith(
expect.objectContaining({ toolBindings: { ordinaryBinding: "ordinary" } }),
);
expect(otherFactory.mock.calls[0]?.[0]).not.toHaveProperty("pluginToolBindings");
});
it("isolates tools with malformed required client capabilities", () => {
const registry = setRegistry([
{
+7 -1
View File
@@ -250,8 +250,14 @@ function resolvePluginToolFactory(
pluginRegistry: PluginRegistry | undefined,
ctx: OpenClawPluginToolContext,
) {
const { pluginToolBindings, ...context } = ctx;
const scopedBindings = pluginToolBindings?.[entry.pluginId];
return runWithPluginToolScope(entry, pluginRegistry, () =>
wrapPluginToolFactoryResult(entry, pluginRegistry, entry.factory(ctx)),
wrapPluginToolFactoryResult(
entry,
pluginRegistry,
entry.factory(scopedBindings ? { ...context, toolBindings: scopedBindings } : context),
),
);
}
+5 -2
View File
@@ -1417,10 +1417,13 @@ BEGIN
END;
CREATE TABLE IF NOT EXISTS memory_postbox_settings (
agent_id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL,
target_store_id TEXT NOT NULL,
mode TEXT NOT NULL CHECK (mode IN ('off', 'review-required')),
updated_by_principal_id TEXT NOT NULL,
updated_at INTEGER NOT NULL
updated_at INTEGER NOT NULL,
PRIMARY KEY (agent_id, target_store_id),
FOREIGN KEY (target_store_id) REFERENCES memory_stores(store_id) ON DELETE RESTRICT
) STRICT;
CREATE TABLE IF NOT EXISTS memory_postbox_source_handles (