mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 22:11:37 +00:00
* refactor(sessions): migrate runtime storage to sqlite * test(sessions): fix sqlite CI regressions * test(sessions): align remaining sqlite fixtures * fix(codex): require sqlite trajectory recorder * test(sessions): align orphan recovery sqlite fixture * test(sessions): align sqlite rebase fixtures * fix(sessions): finish current-main integration of the sqlite flip Resolve the whole-store SDK removal across its owner boundary: drop the loadSessionStore re-export and the registry whole-store wrappers, wire hasTrackedActiveSessionRun into gateway chat, complete the preserveLockedHarnessIds cleanup contract, flip the codex thread-history import to storePath targets, and port remaining main-side tests from file-store helpers to session accessor reads. * chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain bump gets its own review, and regenerate docs_map, the plugin SDK API baseline, and the export-surface ratchet for the merged tree. * feat(sessions): keep archived transcripts by default with zstd cold storage Codex-style retention: deleting or resetting a session archives its transcript as a zstd-compressed JSONL artifact (plain when the runtime lacks node:zlib zstd) and keeps it until the disk budget evicts oldest first. resetArchiveRetention now governs both deleted and reset archives and defaults to keep; maxDiskBytes defaults to 2gb so retention stays bounded, with archives evicted before live sessions. The cron reaper follows the same knob instead of deleting archives on its own timer. * fix(state): converge agent DB migration lineages and bound database growth Merge coherence: run both structure-gated legacy memory-schema repairs (flip-lineage drop, main-lineage identity rebuild) before the flip migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all converge, and hoist foreign_keys=OFF outside the schema transaction where the pragma was silently ignored and the v1 sessions rebuild cascade-deleted session_entries. Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL maintenance releases freed pages in bounded passes (never a blocking full VACUUM), and doctor reports state/agent DB bloat from freelist stats. * fix(codex): resolve the store path for thread-history import via the SDK The supervision catalog passed the legacy sessionFile locator to the storePath-targeted transcript mirror; resolve the agent store path with the session-store SDK helper instead of a runtime-object seam so test fakes and headless callers need no extra surface. Drop the obsolete missing-session-id preprocessing case: sessions rows are NOT NULL on session_id and upsert repairs id-less patches at write time. * fix(sessions): fail safe on malformed disk-budget config and doctor stat errors A malformed explicit maxDiskBytes disables the budget instead of falling back to the destructive 2gb default the user never chose, and the doctor bloat check skips databases whose paths stat-fail instead of aborting doctor. * fix(sessions): complete sqlite conflict translations * test(sqlite): align hardening checks with maintenance * test(sessions): inspect compressed transcript archives * fix(tests): await session seeds and drop unused helpers flagged by CI lint The five unawaited writeSessionStoreSeed calls raced their SQLite seeds against the assertions, failing compact shards; the bloat probe drops a useless initializer and the merged tests drop now-unused helpers. * test(sessions): type legacy proof events directly * test(sessions): align hardening contracts * perf(sessions): read usage transcript sizes from SQL aggregates Usage/cost scans walked every session and materialized every transcript event just to re-stringify it for a byte estimate — the #86718 stall class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes in SQLite without loading a single row. * fix(sessions): re-root foreign-root transcript paths onto the current sessions dir Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry absolute sessionFile paths from the old root; the containment fallback kept those foreign paths, so migration read (and would archive) files in the original root and reported local copies missing. Re-root the canonical agents/<id>/sessions suffix onto the current dir when the file exists there; genuine cross-root layouts still fall through unchanged. * test(agents): seed harness admission through sqlite * fix(sqlite): close agent db on pragma setup failure * fix(doctor): compact and retrofit incremental auto-vacuum after session import The migration is the sanctioned offline window: post-import compact reclaims import churn and applies auto_vacuum=INCREMENTAL to databases created before the fresh-DB pragma existed, so runtime maintenance can release pages in bounded passes on every install. --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
493 lines
14 KiB
TypeScript
493 lines
14 KiB
TypeScript
// Matrix tests cover exec approvals plugin behavior.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
|
import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
getMatrixExecApprovalApprovers,
|
|
isMatrixExecApprovalApprover,
|
|
isMatrixExecApprovalAuthorizedSender,
|
|
isMatrixExecApprovalClientEnabled,
|
|
isMatrixExecApprovalTargetRecipient,
|
|
normalizeMatrixApproverId,
|
|
resolveMatrixExecApprovalTarget,
|
|
shouldHandleMatrixExecApprovalRequest,
|
|
shouldSuppressLocalMatrixExecApprovalPrompt,
|
|
} from "./exec-approvals.js";
|
|
import type { MatrixAccountConfig, MatrixExecApprovalConfig } from "./types.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
type MatrixExecApprovalRequest = Parameters<
|
|
typeof shouldHandleMatrixExecApprovalRequest
|
|
>[0]["request"];
|
|
|
|
afterEach(() => {
|
|
closeOpenClawAgentDatabasesForTest();
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function createTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-exec-approvals-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function buildConfig(
|
|
execApprovals?: NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>["execApprovals"],
|
|
channelOverrides?: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>>,
|
|
): OpenClawConfig {
|
|
return {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
...channelOverrides,
|
|
execApprovals,
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
}
|
|
|
|
function matrixAccount(
|
|
accountId: string,
|
|
execApprovals: MatrixExecApprovalConfig,
|
|
overrides: Partial<MatrixAccountConfig> = {},
|
|
): MatrixAccountConfig {
|
|
return {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: `@bot-${accountId}:example.org`,
|
|
accessToken: `tok-${accountId}`,
|
|
...overrides,
|
|
execApprovals,
|
|
};
|
|
}
|
|
|
|
function buildMultiAccountMatrixConfig(params: {
|
|
sessionStorePath?: string;
|
|
defaultExecApprovals?: MatrixExecApprovalConfig;
|
|
opsExecApprovals?: MatrixExecApprovalConfig;
|
|
defaultOverrides?: Partial<MatrixAccountConfig>;
|
|
opsOverrides?: Partial<MatrixAccountConfig>;
|
|
}): OpenClawConfig {
|
|
return {
|
|
...(params.sessionStorePath ? { session: { store: params.sessionStorePath } } : {}),
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
default: matrixAccount(
|
|
"default",
|
|
params.defaultExecApprovals ?? {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
params.defaultOverrides,
|
|
),
|
|
ops: matrixAccount(
|
|
"ops",
|
|
params.opsExecApprovals ?? {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
params.opsOverrides,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
}
|
|
|
|
function makeForeignChannelApprovalRequest(params: {
|
|
id: string;
|
|
sessionKey?: string;
|
|
agentId?: string;
|
|
}): MatrixExecApprovalRequest {
|
|
return {
|
|
id: params.id,
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: params.agentId ?? "ops-agent",
|
|
sessionKey: params.sessionKey ?? "agent:ops-agent:missing",
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "channel:C123",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
};
|
|
}
|
|
|
|
describe("matrix exec approvals", () => {
|
|
it("requires enablement and approvers before enabling the client", () => {
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig(undefined, { dm: { allowFrom: ["@owner:example.org"] } }),
|
|
}),
|
|
).toBe(false);
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig({ enabled: true }, { dm: { allowFrom: ["@owner:example.org"] } }),
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("prefers explicit approvers when configured", () => {
|
|
const cfg = buildConfig(
|
|
{ enabled: true, approvers: ["user:@override:example.org"] },
|
|
{ dm: { allowFrom: ["@owner:example.org"] } },
|
|
);
|
|
|
|
expect(getMatrixExecApprovalApprovers({ cfg })).toEqual(["@override:example.org"]);
|
|
expect(isMatrixExecApprovalApprover({ cfg, senderId: "@override:example.org" })).toBe(true);
|
|
expect(isMatrixExecApprovalApprover({ cfg, senderId: "@owner:example.org" })).toBe(false);
|
|
});
|
|
|
|
it("ignores wildcard allowlist entries when inferring exec approvers", () => {
|
|
const cfg = buildConfig({ enabled: true }, { dm: { allowFrom: ["*"] } });
|
|
|
|
expect(getMatrixExecApprovalApprovers({ cfg })).toStrictEqual([]);
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg })).toBe(false);
|
|
});
|
|
|
|
it("defaults target to dm", () => {
|
|
expect(
|
|
resolveMatrixExecApprovalTarget({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
}),
|
|
).toBe("dm");
|
|
});
|
|
|
|
it("matches matrix target recipients from generic approval forwarding targets", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
},
|
|
},
|
|
approvals: {
|
|
exec: {
|
|
enabled: true,
|
|
mode: "targets",
|
|
targets: [
|
|
{ channel: "matrix", to: "user:@target:example.org" },
|
|
{ channel: "matrix", to: "room:!ops:example.org" },
|
|
],
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@target:example.org" })).toBe(
|
|
true,
|
|
);
|
|
expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@other:example.org" })).toBe(
|
|
false,
|
|
);
|
|
expect(isMatrixExecApprovalAuthorizedSender({ cfg, senderId: "@target:example.org" })).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("suppresses local prompts only when the native client is enabled", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
payload,
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig(),
|
|
payload,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps local prompts when filters exclude the request", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
agentId: "other-agent",
|
|
sessionKey: "agent:other-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
}),
|
|
payload,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("suppresses local prompts for generic exec payloads when metadata matches filters", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
approvalKind: "exec",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
sessionFilter: ["matrix:channel:"],
|
|
}),
|
|
payload,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("suppresses local prompts for plugin approval payloads when DM approvers are configured", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "plugin:req-1",
|
|
approvalSlug: "plugin:r",
|
|
approvalKind: "plugin",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig(
|
|
{ enabled: true, approvers: ["@owner:example.org"] },
|
|
{ dm: { allowFrom: ["@owner:example.org"] } },
|
|
),
|
|
payload,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("normalizes prefixed approver ids", () => {
|
|
expect(normalizeMatrixApproverId("matrix:@owner:example.org")).toBe("@owner:example.org");
|
|
expect(normalizeMatrixApproverId("user:@owner:example.org")).toBe("@owner:example.org");
|
|
});
|
|
|
|
it("applies agent and session filters to request handling", () => {
|
|
const cfg = buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
sessionFilter: ["matrix:channel:", "ops$"],
|
|
});
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
request: {
|
|
id: "req-1",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org:ops",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
request: {
|
|
id: "req-2",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "other-agent",
|
|
sessionKey: "agent:other-agent:matrix:channel:!room:example.org:ops",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
},
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("scopes non-matrix turn sources to the stored matrix account", async () => {
|
|
const tmpDir = createTempDir();
|
|
const storePath = path.join(tmpDir, "sessions.json");
|
|
await upsertSessionEntry({
|
|
storePath,
|
|
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
|
|
entry: {
|
|
sessionId: "main",
|
|
updatedAt: 1,
|
|
origin: {
|
|
provider: "matrix",
|
|
accountId: "ops",
|
|
to: "room:!room:example.org",
|
|
nativeChannelId: "!room:example.org",
|
|
},
|
|
deliveryContext: {
|
|
channel: "matrix",
|
|
to: "room:!room:example.org",
|
|
accountId: "ops",
|
|
},
|
|
lastChannel: "slack",
|
|
lastTo: "channel:C999",
|
|
lastAccountId: "work",
|
|
},
|
|
});
|
|
const cfg = buildMultiAccountMatrixConfig({ sessionStorePath: storePath });
|
|
const request = makeForeignChannelApprovalRequest({
|
|
id: "req-3",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
|
|
});
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects unbound foreign-channel approvals in multi-account matrix configs", () => {
|
|
const cfg = buildMultiAccountMatrixConfig({});
|
|
const request = makeForeignChannelApprovalRequest({ id: "req-4" });
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("allows unbound foreign-channel approvals when only one matrix account can handle them", () => {
|
|
const cfg = buildMultiAccountMatrixConfig({
|
|
opsExecApprovals: {
|
|
enabled: false,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
});
|
|
const request = makeForeignChannelApprovalRequest({ id: "req-5" });
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("uses request filters when checking foreign-channel matrix ambiguity", () => {
|
|
const cfg = buildMultiAccountMatrixConfig({
|
|
defaultExecApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
},
|
|
opsExecApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["other-agent"],
|
|
},
|
|
});
|
|
const request = makeForeignChannelApprovalRequest({ id: "req-6" });
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ignores disabled matrix accounts when checking foreign-channel ambiguity", () => {
|
|
const cfg = buildMultiAccountMatrixConfig({
|
|
opsOverrides: { enabled: false },
|
|
});
|
|
const request = makeForeignChannelApprovalRequest({ id: "req-7" });
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|