mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-19 04:42:01 +08:00
Compare commits
13 Commits
fix/securi
...
fix/pr-mac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94672cf1f5 | ||
|
|
be4c541176 | ||
|
|
cbf6f0001b | ||
|
|
bda7581126 | ||
|
|
e349bdb949 | ||
|
|
768704e906 | ||
|
|
ba1403604d | ||
|
|
e939963784 | ||
|
|
5ff7242391 | ||
|
|
c25a4e6d0b | ||
|
|
4ea1b4fc4a | ||
|
|
3881cb3426 | ||
|
|
bfd11ee29f |
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Channels and delivery: preserve account-scoped DM channel send policy, intentional rich-message line breaks in Telegram and status output, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Feishu dynamic-agent routes after persisted binding reuse, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #93164, #92679, #89421, #89943, #42837, #92814, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @liuhao1024, @lundog, @TurboTheTurtle, and @yhterrance.
|
||||
- Gemini CLI: use the selected OpenClaw OAuth/API-key auth profile in an isolated Gemini CLI runtime home, preventing ambient Google machine credentials from overriding the chosen profile. (#88748) Thanks @jason-allen-oneal and @shakkernerd.
|
||||
- Feishu: fetch quoted/replied message content before the empty-message guard so a mention-only reply that quotes a message with meaningful content is no longer dropped. (#90192) Thanks @bladin.
|
||||
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
|
||||
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, clamp trusted subagent thinking overrides through provider/model fallback, preserve yielded media completions, deliver channel message-tool final replies through auto-reply while hiding internal delivery hints, restore reset archive fallback reads when active async transcripts are missing, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions, slash-command block replies, and trajectory export commands in WebChat, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92412, #92146, #92879, #91287, #92468, #92510, #91246, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @masatohoshino, @CadanHu, @ZengWen-DT, @zhangguiping-xydt, and @TurboTheTurtle.
|
||||
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, recover invalid OpenAI reasoning signatures and genericized Anthropic thinking-signature replay errors, route OAuth image defaults through Codex for eligible OpenAI profiles, avoid eager tool streaming for Claude 4.5 in Copilot, quarantine unreadable and post-hook OpenAI/Anthropic-family tool schemas without broadening allowed tool choices, deliver explicit thinking-off requests to LM Studio binary-thinking models, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #92941, #92201, #92916, #92824, #75393, #92908, #92921, #92928, #92002, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @mmyzwl, @CarlCapital, @bek91, @Kailigithub, @vincentkoc, @rohitjavvadi, @samson910022, @nxmxbbd, @liuhao1024, @bymle, and @mushuiyu886.
|
||||
|
||||
@@ -424,6 +424,7 @@ async function dispatchMessage(params: {
|
||||
currentCfg?: ClawdbotConfig;
|
||||
event: FeishuMessageEvent;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
botOpenId?: string;
|
||||
}) {
|
||||
const runtime = createRuntimeEnv();
|
||||
const feishuConfig = params.cfg.channels?.feishu;
|
||||
@@ -444,6 +445,7 @@ async function dispatchMessage(params: {
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event: params.event,
|
||||
botOpenId: params.botOpenId,
|
||||
runtime,
|
||||
channelRuntime: params.channelRuntime,
|
||||
});
|
||||
@@ -4164,6 +4166,150 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
// No reply should be dispatched: empty message is silently skipped
|
||||
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not drop empty-text message when it quotes a parent message (#90177)", async () => {
|
||||
// A Feishu reply containing only @bot (no additional text) was being
|
||||
// dropped before the quoted message content was fetched. The handler
|
||||
// should fetch quoted content first and only skip if all of current
|
||||
// text, media, and quoted content are empty.
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
mockGetMessageFeishu.mockResolvedValueOnce({
|
||||
messageId: "om_quoted_001",
|
||||
chatId: "oc-dm",
|
||||
content: "quoted message content from parent",
|
||||
contentType: "text",
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-reply-only-bot",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-empty-with-quote",
|
||||
parent_id: "om_quoted_001",
|
||||
chat_id: "oc-dm",
|
||||
chat_type: "p2p",
|
||||
message_type: "text",
|
||||
// Empty text — only @bot mention, no additional content
|
||||
content: JSON.stringify({ text: "" }),
|
||||
},
|
||||
};
|
||||
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
// A reply should be dispatched because quoted content provides context
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dispatches mention-only group reply with quoted content in requireMention:true group (#90177)", async () => {
|
||||
// #90177 is specifically about group chats. The empty-message drop happens
|
||||
// after the group admission/mention gate, so the fix must also work when
|
||||
// the sender mentions the bot in a requireMention:true group and quotes a
|
||||
// parent message with meaningful content — the reply should dispatch with
|
||||
// the quoted text in the body.
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
mockGetMessageFeishu.mockResolvedValueOnce({
|
||||
messageId: "om_group_quoted_001",
|
||||
chatId: "oc-group-90177",
|
||||
content: "parent message with context",
|
||||
contentType: "text",
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
groupPolicy: "open",
|
||||
groups: {
|
||||
"oc-group-90177": {
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-group-sender",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-group-empty-with-quote",
|
||||
parent_id: "om_group_quoted_001",
|
||||
chat_id: "oc-group-90177",
|
||||
chat_type: "group",
|
||||
message_type: "text",
|
||||
// Empty text — only @bot mention, no additional content
|
||||
content: JSON.stringify({ text: "" }),
|
||||
// Bot mention so the message passes the requireMention gate
|
||||
mentions: [
|
||||
{ key: "@_bot_1", id: { open_id: "ou-bot-90177" }, name: "Bot", tenant_key: "" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await dispatchMessage({ cfg, event, botOpenId: "ou-bot-90177" });
|
||||
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
const context = mockCallArg<{ Body?: string }>(mockFinalizeInboundContext, 0, 0);
|
||||
expect(context.Body).toContain("[Replying to:");
|
||||
expect(context.Body).toContain("parent message with context");
|
||||
});
|
||||
|
||||
it("does not over-fetch quoted message for unmentioned empty reply in requireMention:true group (#90177)", async () => {
|
||||
// An empty-text reply that quotes a parent but does NOT mention the bot
|
||||
// in a requireMention:true group should be rejected at the mention gate
|
||||
// before the quoted message is fetched, so getMessageFeishu is never
|
||||
// called and nothing is dispatched.
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
groupPolicy: "open",
|
||||
groups: {
|
||||
"oc-group-90177-neg": {
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-group-sender-neg",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-group-unmentioned-empty-quote",
|
||||
parent_id: "om_group_quoted_neg",
|
||||
chat_id: "oc-group-90177-neg",
|
||||
chat_type: "group",
|
||||
message_type: "text",
|
||||
// Empty text with no bot mention
|
||||
content: JSON.stringify({ text: "" }),
|
||||
},
|
||||
};
|
||||
|
||||
await dispatchMessage({ cfg, event, botOpenId: "ou-bot-90177-neg" });
|
||||
|
||||
expect(mockGetMessageFeishu).not.toHaveBeenCalled();
|
||||
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFeishuMessageReceiveHandler media dedupe", () => {
|
||||
|
||||
@@ -1026,15 +1026,57 @@ export async function handleFeishuMessage(params: {
|
||||
log,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
// Skip messages with no text content and no media attachments. Feishu can
|
||||
// deliver empty-text events (e.g. `{"text":""}`) when a user sends a blank
|
||||
// message or when media parsing produces an empty string. Writing a blank
|
||||
// user turn to the session causes downstream LLM providers (e.g. MiniMax)
|
||||
// to reject the request with "messages must not be empty" errors. Logging
|
||||
// the skip avoids silent loss without polluting the agent session.
|
||||
if (!ctx.content.trim() && mediaList.length === 0) {
|
||||
// Fetch quoted/replied message content before the empty-message guard
|
||||
// so a reply with only @bot (no text, no media) is not dropped when
|
||||
// the quoted message carries meaningful content.
|
||||
let quotedMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> = null;
|
||||
let quotedContent: string | undefined;
|
||||
if (ctx.parentId) {
|
||||
try {
|
||||
quotedMessageInfo = await getMessageFeishu({
|
||||
cfg,
|
||||
messageId: ctx.parentId,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
if (
|
||||
quotedMessageInfo &&
|
||||
(await shouldIncludeFetchedGroupContextMessage({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
isGroup,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
mode: contextVisibilityMode,
|
||||
kind: "quote",
|
||||
senderId: quotedMessageInfo.senderId,
|
||||
senderType: quotedMessageInfo.senderType,
|
||||
}))
|
||||
) {
|
||||
quotedContent = quotedMessageInfo.content;
|
||||
log(
|
||||
`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
|
||||
);
|
||||
} else if (quotedMessageInfo) {
|
||||
log(
|
||||
`feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip messages with no text content, no media attachments, and no quoted
|
||||
// content. Feishu can deliver empty-text events (e.g. `{"text":""}`) when
|
||||
// a user sends a blank message or when media parsing produces an empty
|
||||
// string. Writing a blank user turn to the session causes downstream LLM
|
||||
// providers (e.g. MiniMax) to reject the request with "messages must not
|
||||
// be empty" errors. Logging the skip avoids silent loss without polluting
|
||||
// the agent session. Quoted content is checked too so a reply-only @bot
|
||||
// with quoted context is not dropped.
|
||||
if (!ctx.content.trim() && mediaList.length === 0 && !quotedContent?.trim()) {
|
||||
log(
|
||||
`feishu[${account.accountId}]: skipping empty message (no text, no media) from ${ctx.senderOpenId}`,
|
||||
`feishu[${account.accountId}]: skipping empty message (no text, no media, no quoted) from ${ctx.senderOpenId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1107,44 +1149,6 @@ export async function handleFeishuMessage(params: {
|
||||
).commandAccess.authorized
|
||||
: undefined;
|
||||
|
||||
// Fetch quoted/replied message content if parentId exists
|
||||
let quotedMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> = null;
|
||||
let quotedContent: string | undefined;
|
||||
if (ctx.parentId) {
|
||||
try {
|
||||
quotedMessageInfo = await getMessageFeishu({
|
||||
cfg,
|
||||
messageId: ctx.parentId,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
if (
|
||||
quotedMessageInfo &&
|
||||
(await shouldIncludeFetchedGroupContextMessage({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
isGroup,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
mode: contextVisibilityMode,
|
||||
kind: "quote",
|
||||
senderId: quotedMessageInfo.senderId,
|
||||
senderType: quotedMessageInfo.senderType,
|
||||
}))
|
||||
) {
|
||||
quotedContent = quotedMessageInfo.content;
|
||||
log(
|
||||
`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
|
||||
);
|
||||
} else if (quotedMessageInfo) {
|
||||
log(
|
||||
`feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isTopicSessionForThread =
|
||||
isGroup &&
|
||||
(groupSession?.groupSessionScope === "group_topic" ||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Feishu client module import behavior tests.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("@larksuiteoapi/node-sdk");
|
||||
vi.doUnmock("@openclaw/proxyline");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe("Feishu client module", () => {
|
||||
it("loads when the SDK has no default HTTP instance", async () => {
|
||||
vi.doMock("@larksuiteoapi/node-sdk", () => ({
|
||||
AppType: { SelfBuild: "self" },
|
||||
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
|
||||
LoggerLevel: { info: "info" },
|
||||
Client: vi.fn(),
|
||||
WSClient: vi.fn(),
|
||||
EventDispatcher: vi.fn(),
|
||||
defaultHttpInstance: undefined,
|
||||
}));
|
||||
vi.doMock("@openclaw/proxyline", () => ({
|
||||
createAmbientNodeProxyAgent: vi.fn(),
|
||||
hasAmbientNodeProxyConfigured: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
await expect(import("./client.js")).resolves.toMatchObject({
|
||||
createFeishuClient: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -387,23 +387,6 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects client creation when the SDK default HTTP instance is unavailable", () => {
|
||||
setFeishuClientRuntimeForTest({
|
||||
sdk: {
|
||||
defaultHttpInstance: undefined as never,
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
createFeishuClient({
|
||||
appId: "app-default-http",
|
||||
appSecret: "secret-default-http", // pragma: allowlist secret
|
||||
accountId: "default-http-instance",
|
||||
}),
|
||||
).toThrow("Feishu SDK default HTTP instance is unavailable");
|
||||
expect(clientCtorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("evicts client cache when SDK is replaced via setFeishuClientRuntimeForTest (#83911)", () => {
|
||||
const ctorCountA = clientCtorMock.mock.calls.length;
|
||||
|
||||
|
||||
@@ -67,14 +67,12 @@ let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
|
||||
// If a future SDK version adds more interceptors, the upgrade will need
|
||||
// compatibility verification regardless.
|
||||
{
|
||||
const inst = Lark.defaultHttpInstance as
|
||||
| {
|
||||
interceptors?: {
|
||||
request: { handlers: unknown[]; use: (fn: (req: unknown) => unknown) => void };
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (inst?.interceptors?.request) {
|
||||
const inst = Lark.defaultHttpInstance as {
|
||||
interceptors?: {
|
||||
request: { handlers: unknown[]; use: (fn: (req: unknown) => unknown) => void };
|
||||
};
|
||||
};
|
||||
if (inst.interceptors?.request) {
|
||||
inst.interceptors.request.handlers = [];
|
||||
inst.interceptors.request.use((req: unknown) => {
|
||||
const r = req as { headers?: Record<string, string> };
|
||||
@@ -121,10 +119,9 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
* but injects a default request timeout and User-Agent header to prevent
|
||||
* indefinite hangs and set a standardized User-Agent per OAPI best practices.
|
||||
*/
|
||||
function createTimeoutHttpInstance(
|
||||
base: FeishuHttpInstanceLike,
|
||||
defaultTimeoutMs: number,
|
||||
): Lark.HttpInstance {
|
||||
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
|
||||
const base: FeishuHttpInstanceLike = feishuClientSdk.defaultHttpInstance;
|
||||
|
||||
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
|
||||
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
|
||||
}
|
||||
@@ -178,19 +175,13 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
||||
return cached.client;
|
||||
}
|
||||
|
||||
const defaultHttpInstance = feishuClientSdk.defaultHttpInstance as
|
||||
| FeishuHttpInstanceLike
|
||||
| undefined;
|
||||
if (!defaultHttpInstance) {
|
||||
throw new Error("Feishu SDK default HTTP instance is unavailable");
|
||||
}
|
||||
|
||||
// Create new client with timeout-aware HTTP instance
|
||||
const client = new feishuClientSdk.Client({
|
||||
appId,
|
||||
appSecret,
|
||||
appType: feishuClientSdk.AppType.SelfBuild,
|
||||
domain: resolveDomain(domain),
|
||||
httpInstance: createTimeoutHttpInstance(defaultHttpInstance, defaultHttpTimeoutMs),
|
||||
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs),
|
||||
});
|
||||
|
||||
// Cache it
|
||||
|
||||
10
extensions/matrix/npm-shrinkwrap.json
generated
10
extensions/matrix/npm-shrinkwrap.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "@openclaw/matrix",
|
||||
"version": "2026.6.8",
|
||||
"dependencies": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "0.4.0",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "0.6.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.3.0",
|
||||
"fake-indexeddb": "6.2.5",
|
||||
"markdown-it": "14.2.0",
|
||||
@@ -46,9 +46,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@matrix-org/matrix-sdk-crypto-nodejs": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.4.0.tgz",
|
||||
"integrity": "sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.6.0.tgz",
|
||||
"integrity": "sha512-AndGryzkDtFbaDyPBAQ2B4pUhaA/q4HJf3wgiGpPa/70DsdY1Z3R5Wn9yp+56CeHOpk61mNHz/8WDPlzrZDSJw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"node-downloader-helper": "^2.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
"node": ">= 24"
|
||||
}
|
||||
},
|
||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "0.4.0",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "0.6.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.3.0",
|
||||
"fake-indexeddb": "6.2.5",
|
||||
"markdown-it": "14.2.0",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -957,8 +957,8 @@ importers:
|
||||
extensions/matrix:
|
||||
dependencies:
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs':
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0
|
||||
specifier: 0.6.0
|
||||
version: 0.6.0
|
||||
'@matrix-org/matrix-sdk-crypto-wasm':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
@@ -2926,9 +2926,9 @@ packages:
|
||||
'@lydell/node-pty@1.2.0-beta.12':
|
||||
resolution: {integrity: sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==}
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs@0.4.0':
|
||||
resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==}
|
||||
engines: {node: '>= 22'}
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs@0.6.0':
|
||||
resolution: {integrity: sha512-AndGryzkDtFbaDyPBAQ2B4pUhaA/q4HJf3wgiGpPa/70DsdY1Z3R5Wn9yp+56CeHOpk61mNHz/8WDPlzrZDSJw==}
|
||||
engines: {node: '>= 24'}
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.3.0':
|
||||
resolution: {integrity: sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==}
|
||||
@@ -8873,7 +8873,7 @@ snapshots:
|
||||
'@lydell/node-pty-win32-arm64': 1.2.0-beta.12
|
||||
'@lydell/node-pty-win32-x64': 1.2.0-beta.12
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs@0.4.0':
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs@0.6.0':
|
||||
dependencies:
|
||||
https-proxy-agent: 7.0.6
|
||||
node-downloader-helper: 2.1.11
|
||||
|
||||
@@ -14,6 +14,14 @@ if common_git_dir=$(git -C "$script_parent_dir" rev-parse --path-format=absolute
|
||||
fi
|
||||
fi
|
||||
|
||||
# This wrapper parses GitHub CLI JSON with jq. Ignore interactive color settings
|
||||
# inherited from operator shells so machine output stays valid JSON.
|
||||
export NO_COLOR=1
|
||||
export CLICOLOR=0
|
||||
export CLICOLOR_FORCE=0
|
||||
unset GH_FORCE_TTY
|
||||
export GH_PAGER=cat
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage:
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Model } from "../../llm/types.js";
|
||||
import { createSessionManagerRuntimeRegistry } from "./session-manager-runtime-registry.js";
|
||||
|
||||
/** Runtime knobs consumed by the compaction safeguard extension. */
|
||||
export type CompactionSafeguardRuntimeValue = {
|
||||
type CompactionSafeguardRuntimeValue = {
|
||||
maxHistoryShare?: number;
|
||||
contextWindowTokens?: number;
|
||||
identifierPolicy?: AgentCompactionIdentifierPolicy;
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
export {
|
||||
AUTH_PROFILE_FILENAME,
|
||||
AUTH_STATE_FILENAME,
|
||||
LEGACY_AUTH_FILENAME,
|
||||
} from "./path-constants.js";
|
||||
|
||||
/** Current persisted auth profile store schema version. */
|
||||
|
||||
@@ -208,7 +208,4 @@ export function syncPersistedExternalCliAuthProfiles(
|
||||
return next ?? store;
|
||||
}
|
||||
|
||||
// Compat aliases while file/function naming catches up.
|
||||
export const overlayExternalOAuthProfiles = overlayExternalAuthProfiles;
|
||||
export const shouldPersistExternalOAuthProfile = shouldPersistExternalAuthProfile;
|
||||
export { testing as __testing };
|
||||
|
||||
@@ -7,8 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderExternalAuthProfile } from "../../plugins/types.js";
|
||||
import {
|
||||
testing,
|
||||
overlayExternalOAuthProfiles,
|
||||
shouldPersistExternalOAuthProfile,
|
||||
overlayExternalAuthProfiles,
|
||||
shouldPersistExternalAuthProfile,
|
||||
} from "./external-auth.js";
|
||||
import { readManagedExternalCliCredential } from "./external-cli-sync.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
@@ -78,7 +78,7 @@ describe("auth external oauth helpers", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const store = overlayExternalOAuthProfiles(createStore());
|
||||
const store = overlayExternalAuthProfiles(createStore());
|
||||
|
||||
const profile = requireProfile(store, "openai:default");
|
||||
expect(profile.type).toBe("oauth");
|
||||
@@ -94,7 +94,7 @@ describe("auth external oauth helpers", () => {
|
||||
};
|
||||
readCodexCliCredentialsCachedMock.mockReturnValueOnce(createCredential());
|
||||
|
||||
overlayExternalOAuthProfiles(createStore(), {
|
||||
overlayExternalAuthProfiles(createStore(), {
|
||||
allowKeychainPrompt: false,
|
||||
config: cfg,
|
||||
externalCliProviderIds: ["openai"],
|
||||
@@ -128,7 +128,7 @@ describe("auth external oauth helpers", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(store);
|
||||
const overlaid = overlayExternalAuthProfiles(store);
|
||||
|
||||
expect(readCodexCliCredentialsCachedMock).not.toHaveBeenCalled();
|
||||
expect(overlaid.profiles["openai:work"]).toEqual(store.profiles["openai:work"]);
|
||||
@@ -143,7 +143,7 @@ describe("auth external oauth helpers", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
const shouldPersist = shouldPersistExternalAuthProfile({
|
||||
store: createStore({ "openai:default": credential }),
|
||||
profileId: "openai:default",
|
||||
credential,
|
||||
@@ -162,7 +162,7 @@ describe("auth external oauth helpers", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
const shouldPersist = shouldPersistExternalAuthProfile({
|
||||
store: createStore({ "openai:default": credential }),
|
||||
profileId: "openai:default",
|
||||
credential,
|
||||
@@ -180,7 +180,7 @@ describe("auth external oauth helpers", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
const shouldPersist = shouldPersistExternalAuthProfile({
|
||||
store: createStore({ "openai:default": credential }),
|
||||
profileId: "openai:default",
|
||||
credential,
|
||||
@@ -199,7 +199,7 @@ describe("auth external oauth helpers", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(
|
||||
const overlaid = overlayExternalAuthProfiles(
|
||||
createStore({
|
||||
"openai:default": createCredential({
|
||||
access: "stale-store-access-token",
|
||||
@@ -231,7 +231,7 @@ describe("auth external oauth helpers", () => {
|
||||
} as OAuthCredential;
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue(cliCredential);
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(
|
||||
const overlaid = overlayExternalAuthProfiles(
|
||||
createStore({
|
||||
"openai:default": tokenlessCredential,
|
||||
}),
|
||||
@@ -263,7 +263,7 @@ describe("auth external oauth helpers", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(
|
||||
const overlaid = overlayExternalAuthProfiles(
|
||||
createStore({
|
||||
"openai:default": createCredential({
|
||||
access: "healthy-local-access-token",
|
||||
@@ -287,7 +287,7 @@ describe("auth external oauth helpers", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(
|
||||
const overlaid = overlayExternalAuthProfiles(
|
||||
createStore({
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
@@ -313,7 +313,7 @@ describe("auth external oauth helpers", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const overlaid = overlayExternalOAuthProfiles(
|
||||
const overlaid = overlayExternalAuthProfiles(
|
||||
createStore({
|
||||
"openai:default": createCredential({
|
||||
access: "expired-local-access-token",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
|
||||
/** Legacy OAuth ref source persisted by older credential stores. */
|
||||
export const LEGACY_OAUTH_REF_SOURCE = "openclaw-credentials";
|
||||
const LEGACY_OAUTH_REF_SOURCE = "openclaw-credentials";
|
||||
/** Legacy OAuth ref provider persisted by older credential stores. */
|
||||
export const LEGACY_OAUTH_REF_PROVIDER = "openai-codex";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_NONCE_MARKER = ":nonce:";
|
||||
const EXEC_APPROVAL_FOLLOWUP_RUNTIME_HANDOFF_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
/** Single-use capability payload consumed by a follow-up agent turn. */
|
||||
export type ExecApprovalFollowupRuntimeHandoff = {
|
||||
type ExecApprovalFollowupRuntimeHandoff = {
|
||||
kind: "exec-approval-followup";
|
||||
approvalId: string;
|
||||
sessionKey: string;
|
||||
@@ -25,7 +25,7 @@ export type ExecApprovalFollowupRuntimeHandoff = {
|
||||
};
|
||||
|
||||
/** Registration handle returned to the gateway approval callback. */
|
||||
export type ExecApprovalFollowupRuntimeHandoffRegistration = {
|
||||
type ExecApprovalFollowupRuntimeHandoffRegistration = {
|
||||
handoffId: string;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ function loadExecApprovalCommandSpansRuntime(): Promise<ExecApprovalCommandSpans
|
||||
}
|
||||
|
||||
/** Gateway payload fields used to register or wait for an exec approval decision. */
|
||||
export type RequestExecApprovalDecisionParams = {
|
||||
type RequestExecApprovalDecisionParams = {
|
||||
id: string;
|
||||
command?: string;
|
||||
commandArgv?: string[];
|
||||
|
||||
@@ -62,7 +62,7 @@ import type {
|
||||
import type { AgentToolResult } from "./runtime/index.js";
|
||||
|
||||
/** Full input bundle for gateway-host allowlist and approval processing. */
|
||||
export type ProcessGatewayAllowlistParams = {
|
||||
type ProcessGatewayAllowlistParams = {
|
||||
command: string;
|
||||
workdir: string;
|
||||
env: Record<string, string>;
|
||||
@@ -104,7 +104,7 @@ export type ProcessGatewayAllowlistParams = {
|
||||
};
|
||||
|
||||
/** Gateway allowlist outcome before command execution continues. */
|
||||
export type ProcessGatewayAllowlistResult = {
|
||||
type ProcessGatewayAllowlistResult = {
|
||||
execCommandOverride?: string;
|
||||
allowWithoutEnforcedCommand?: boolean;
|
||||
pendingResult?: AgentToolResult<ExecToolDetails>;
|
||||
|
||||
@@ -37,8 +37,6 @@ import type { ExecToolDetails } from "./bash-tools.exec-types.js";
|
||||
import type { AgentToolResult } from "./runtime/index.js";
|
||||
import { callGatewayTool } from "./tools/gateway.js";
|
||||
|
||||
export type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js";
|
||||
|
||||
const APPROVED_NODE_INVOKE_SCOPES = [WRITE_SCOPE, APPROVALS_SCOPE];
|
||||
|
||||
function resolveNodeAutoReviewReason(params: {
|
||||
|
||||
8
src/agents/cache/agent-cache-store.sqlite.ts
vendored
8
src/agents/cache/agent-cache-store.sqlite.ts
vendored
@@ -28,13 +28,13 @@ import type {
|
||||
} from "./agent-cache-store.js";
|
||||
|
||||
/** Options for an agent/scope-scoped SQLite runtime cache. */
|
||||
export type SqliteAgentCacheStoreOptions = OpenClawAgentDatabaseOptions & {
|
||||
type SqliteAgentCacheStoreOptions = OpenClawAgentDatabaseOptions & {
|
||||
scope: string;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
/** Options for writing a single SQLite agent cache entry. */
|
||||
export type WriteSqliteAgentCacheEntryOptions = SqliteAgentCacheStoreOptions &
|
||||
type WriteSqliteAgentCacheEntryOptions = SqliteAgentCacheStoreOptions &
|
||||
AgentRuntimeCacheWriteOptions;
|
||||
|
||||
type CacheEntriesTable = OpenClawAgentKyselyDatabase["cache_entries"];
|
||||
@@ -297,7 +297,7 @@ export function clearExpiredSqliteAgentCacheEntries(
|
||||
}
|
||||
|
||||
/** Agent runtime cache store implementation backed by OpenClaw's SQLite DB. */
|
||||
export class SqliteAgentCacheStore implements AgentRuntimeCacheStore {
|
||||
class SqliteAgentCacheStore implements AgentRuntimeCacheStore {
|
||||
readonly #options: SqliteAgentCacheStoreOptions;
|
||||
|
||||
constructor(options: SqliteAgentCacheStoreOptions) {
|
||||
@@ -349,6 +349,6 @@ export class SqliteAgentCacheStore implements AgentRuntimeCacheStore {
|
||||
/** Create a SQLite-backed agent runtime cache store. */
|
||||
export function createSqliteAgentCacheStore(
|
||||
options: SqliteAgentCacheStoreOptions,
|
||||
): SqliteAgentCacheStore {
|
||||
): AgentRuntimeCacheStore {
|
||||
return new SqliteAgentCacheStore(options);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export type AgentAttemptLifecycleState = {
|
||||
};
|
||||
|
||||
/** Event shape emitted by runtimes during an agent attempt. */
|
||||
export type AgentAttemptLifecycleEvent = {
|
||||
type AgentAttemptLifecycleEvent = {
|
||||
stream: string;
|
||||
data?: Record<string, unknown>;
|
||||
sessionKey?: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
/** Parameters for merging and persisting a session entry update. */
|
||||
export type PersistSessionEntryParams = {
|
||||
type PersistSessionEntryParams = {
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
storePath: string;
|
||||
|
||||
@@ -66,10 +66,10 @@ function createRestartOnlyAbortSignal(source: AbortSignal | undefined): {
|
||||
}
|
||||
|
||||
/** Per-payload durable delivery status. */
|
||||
export type AgentCommandDeliveryPayloadStatus = "sent" | "suppressed" | "failed";
|
||||
type AgentCommandDeliveryPayloadStatus = "sent" | "suppressed" | "failed";
|
||||
|
||||
/** Delivery outcome for one normalized outbound payload. */
|
||||
export type AgentCommandDeliveryPayloadOutcome = {
|
||||
type AgentCommandDeliveryPayloadOutcome = {
|
||||
index: number;
|
||||
status: AgentCommandDeliveryPayloadStatus;
|
||||
reason?: string;
|
||||
@@ -84,7 +84,7 @@ export type AgentCommandDeliveryPayloadOutcome = {
|
||||
};
|
||||
|
||||
/** Aggregate delivery status for an agent command result. */
|
||||
export type AgentCommandDeliveryStatus = {
|
||||
type AgentCommandDeliveryStatus = {
|
||||
requested: true;
|
||||
attempted: boolean;
|
||||
status: "sent" | "suppressed" | "partial_failed" | "failed";
|
||||
@@ -100,7 +100,7 @@ export type AgentCommandDeliveryStatus = {
|
||||
};
|
||||
|
||||
/** Agent command result after payload normalization and optional delivery. */
|
||||
export type AgentCommandDeliveryResult = {
|
||||
type AgentCommandDeliveryResult = {
|
||||
payloads: ReturnType<typeof projectOutboundPayloadPlanForJson>;
|
||||
meta: EmbeddedAgentRunMeta & AgentCommandResultMetaOverrides;
|
||||
didSendViaMessagingTool?: boolean;
|
||||
|
||||
@@ -41,7 +41,7 @@ import { clearBootstrapSnapshotOnSessionRollover } from "../bootstrap-cache.js";
|
||||
import { clearAllCliSessions } from "../cli-session.js";
|
||||
|
||||
/** Resolved command session identity plus backing store metadata. */
|
||||
export type SessionResolution = {
|
||||
type SessionResolution = {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
|
||||
@@ -29,7 +29,7 @@ export type AgentCommandResultMetaOverrides = {
|
||||
};
|
||||
|
||||
/** ACP turn source markers accepted by trusted command callsites. */
|
||||
export type AcpTurnSource = "manual_spawn";
|
||||
type AcpTurnSource = "manual_spawn";
|
||||
|
||||
/** Channel/account/thread context carried into an agent run. */
|
||||
export type AgentRunContext = {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { FailoverReason } from "../../embedded-agent-helpers.js";
|
||||
import { log } from "../logger.js";
|
||||
|
||||
/** Structured fields emitted whenever embedded run failover chooses an action. */
|
||||
export type FailoverDecisionLoggerInput = {
|
||||
type FailoverDecisionLoggerInput = {
|
||||
stage: "prompt" | "assistant";
|
||||
decision: "rotate_profile" | "fallback_model" | "surface_error";
|
||||
runId?: string;
|
||||
@@ -31,7 +31,7 @@ export type FailoverDecisionLoggerInput = {
|
||||
};
|
||||
|
||||
/** Stable context captured before a concrete failover decision is known. */
|
||||
export type FailoverDecisionLoggerBase = Omit<FailoverDecisionLoggerInput, "decision" | "status">;
|
||||
type FailoverDecisionLoggerBase = Omit<FailoverDecisionLoggerInput, "decision" | "status">;
|
||||
|
||||
/**
|
||||
* Derives timeout failure reasons for logs that were built from timeout state
|
||||
|
||||
@@ -84,7 +84,7 @@ export type TraceAttempt = {
|
||||
status?: number;
|
||||
};
|
||||
|
||||
export type ExecutionTrace = {
|
||||
type ExecutionTrace = {
|
||||
winnerProvider?: string;
|
||||
winnerModel?: string;
|
||||
attempts?: TraceAttempt[];
|
||||
@@ -92,7 +92,7 @@ export type ExecutionTrace = {
|
||||
runner?: "embedded" | "cli";
|
||||
};
|
||||
|
||||
export type RequestShapingTrace = {
|
||||
type RequestShapingTrace = {
|
||||
authMode?: string;
|
||||
thinking?: string;
|
||||
reasoning?: string;
|
||||
@@ -102,7 +102,7 @@ export type RequestShapingTrace = {
|
||||
blockStreaming?: string;
|
||||
};
|
||||
|
||||
export type PromptSegmentTrace = {
|
||||
type PromptSegmentTrace = {
|
||||
key: string;
|
||||
chars: number;
|
||||
};
|
||||
@@ -114,13 +114,13 @@ export type ToolSummaryTrace = {
|
||||
totalToolTimeMs?: number;
|
||||
};
|
||||
|
||||
export type CompletionTrace = {
|
||||
type CompletionTrace = {
|
||||
finishReason?: string;
|
||||
stopReason?: string;
|
||||
refusal?: boolean;
|
||||
};
|
||||
|
||||
export type ContextManagementTrace = {
|
||||
type ContextManagementTrace = {
|
||||
sessionCompactions?: number;
|
||||
lastTurnCompactions?: number;
|
||||
preflightCompactionApplied?: boolean;
|
||||
|
||||
@@ -145,7 +145,7 @@ export async function awaitAgentHarnessAgentEndHook(params: {
|
||||
}
|
||||
|
||||
/** Normalized before-finalize hook decision consumed by harness loops. */
|
||||
export type AgentHarnessBeforeAgentFinalizeOutcome =
|
||||
type AgentHarnessBeforeAgentFinalizeOutcome =
|
||||
| { action: "continue" }
|
||||
| { action: "revise"; reason: string }
|
||||
| { action: "finalize"; reason?: string };
|
||||
|
||||
@@ -18,7 +18,7 @@ import { buildAgentHookContext, type AgentHarnessHookContext } from "./hook-cont
|
||||
const log = createSubsystemLogger("agents/harness");
|
||||
|
||||
/** Prompt/developer-instruction pair after harness prompt-build hooks run. */
|
||||
export type AgentHarnessPromptBuildResult = {
|
||||
type AgentHarnessPromptBuildResult = {
|
||||
prompt: string;
|
||||
developerInstructions: string;
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ export type AgentHarnessDeliveryDefaults = {
|
||||
sourceVisibleReplies?: "automatic" | "message_tool";
|
||||
};
|
||||
|
||||
export type AgentHarnessRunCapability = {
|
||||
type AgentHarnessRunCapability = {
|
||||
id: string;
|
||||
label: string;
|
||||
pluginId?: string;
|
||||
@@ -98,22 +98,22 @@ export type AgentHarnessRunCapability = {
|
||||
runAttempt(params: AgentHarnessAttemptParams): Promise<AgentHarnessAttemptResult>;
|
||||
};
|
||||
|
||||
export type AgentHarnessSideQuestionCapability = {
|
||||
type AgentHarnessSideQuestionCapability = {
|
||||
runSideQuestion?(params: AgentHarnessSideQuestionParams): Promise<AgentHarnessSideQuestionResult>;
|
||||
};
|
||||
|
||||
export type AgentHarnessClassificationCapability = {
|
||||
type AgentHarnessClassificationCapability = {
|
||||
classify?(
|
||||
result: AgentHarnessAttemptResult,
|
||||
ctx: AgentHarnessAttemptParams,
|
||||
): AgentHarnessResultClassification | undefined;
|
||||
};
|
||||
|
||||
export type AgentHarnessCompactionCapability = {
|
||||
type AgentHarnessCompactionCapability = {
|
||||
compact?(params: AgentHarnessCompactParams): Promise<AgentHarnessCompactResult | undefined>;
|
||||
};
|
||||
|
||||
export type AgentHarnessSessionLifecycleCapability = {
|
||||
type AgentHarnessSessionLifecycleCapability = {
|
||||
reset?(params: AgentHarnessResetParams): Promise<void> | void;
|
||||
dispose?(): Promise<void> | void;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { isAnthropicBillingError, isApiKeyRateLimitError } from "./live-auth-keys.js";
|
||||
import { isModelNotFoundErrorMessage } from "./live-model-errors.js";
|
||||
|
||||
export type LiveProviderDriftReason =
|
||||
type LiveProviderDriftReason =
|
||||
| "auth"
|
||||
| "billing"
|
||||
| "model-not-found"
|
||||
@@ -24,13 +24,13 @@ export type LiveProviderDriftReason =
|
||||
| "timeout";
|
||||
|
||||
/** A normalized reason for skipping or soft-failing live provider drift. */
|
||||
export type LiveProviderDriftDecision = {
|
||||
type LiveProviderDriftDecision = {
|
||||
label: string;
|
||||
reason: LiveProviderDriftReason;
|
||||
};
|
||||
|
||||
/** Classifier options that control which live-provider drift reasons are allowed. */
|
||||
export type LiveProviderDriftOptions = {
|
||||
type LiveProviderDriftOptions = {
|
||||
allowAuth?: boolean;
|
||||
allowBilling?: boolean;
|
||||
allowModelNotFound?: boolean;
|
||||
@@ -41,7 +41,7 @@ export type LiveProviderDriftOptions = {
|
||||
};
|
||||
|
||||
/** Converts arbitrary thrown values into text for provider drift matchers. */
|
||||
export function liveProviderErrorText(error: unknown): string {
|
||||
function liveProviderErrorText(error: unknown): string {
|
||||
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@ export function isLiveRateLimitDrift(error: unknown): boolean {
|
||||
}
|
||||
|
||||
/** Returns whether an error is expected live timeout drift. */
|
||||
export function isLiveTimeoutDrift(error: unknown): boolean {
|
||||
function isLiveTimeoutDrift(error: unknown): boolean {
|
||||
return isTimeoutErrorMessage(liveProviderErrorText(error));
|
||||
}
|
||||
|
||||
/** Returns whether an error is expected live missing-model drift. */
|
||||
export function isLiveModelNotFoundDrift(error: unknown): boolean {
|
||||
function isLiveModelNotFoundDrift(error: unknown): boolean {
|
||||
return isModelNotFoundErrorMessage(liveProviderErrorText(error));
|
||||
}
|
||||
|
||||
|
||||
@@ -141,19 +141,6 @@ function findPersistedTaskForRecentMediaGenerationStart(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns whether a task is an active session-scoped media generation task. */
|
||||
export function isActiveMediaGenerationTask(params: {
|
||||
task: TaskRecord;
|
||||
taskKind: string;
|
||||
}): boolean {
|
||||
return (
|
||||
params.task.runtime === "cli" &&
|
||||
params.task.scopeKind === "session" &&
|
||||
params.task.taskKind === params.taskKind &&
|
||||
(params.task.status === "queued" || params.task.status === "running")
|
||||
);
|
||||
}
|
||||
|
||||
/** Records a just-started media task so duplicate guards work before persistence. */
|
||||
export function recordRecentMediaGenerationTaskStartForSession(params: {
|
||||
sessionKey?: string;
|
||||
@@ -294,7 +281,7 @@ export function resetRecentMediaGenerationDuplicateGuardsForTests() {
|
||||
}
|
||||
|
||||
/** Extracts a provider id from a media task source id with the given prefix. */
|
||||
export function getMediaGenerationTaskProviderId(
|
||||
function getMediaGenerationTaskProviderId(
|
||||
task: TaskRecord,
|
||||
sourcePrefix: string,
|
||||
): string | undefined {
|
||||
|
||||
@@ -41,7 +41,7 @@ export type ResolveImplicitProvidersForModelsJson = (params: {
|
||||
}) => Promise<Record<string, ProviderConfig>>;
|
||||
|
||||
/** Planned models.json write/noop/skip result plus plugin catalog sidecar writes. */
|
||||
export type ModelsJsonPlan =
|
||||
type ModelsJsonPlan =
|
||||
| {
|
||||
action: "skip";
|
||||
pluginCatalogWrites?: Record<string, string>;
|
||||
|
||||
@@ -19,7 +19,7 @@ type ProviderRuntimeModule = typeof import("../plugins/provider-runtime.js");
|
||||
let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_SECRETREF_MARKER;
|
||||
let MINIMAX_OAUTH_MARKER: typeof import("./model-auth-markers.js").MINIMAX_OAUTH_MARKER;
|
||||
let CUSTOM_LOCAL_AUTH_MARKER: typeof import("./model-auth-markers.js").CUSTOM_LOCAL_AUTH_MARKER;
|
||||
let resolveApiKeyFromCredential: typeof import("./models-config.providers.secrets.js").resolveApiKeyFromCredential;
|
||||
let resolveApiKeyFromCredential: typeof import("./models-config.providers.secret-helpers.js").resolveApiKeyFromCredential;
|
||||
let createProviderApiKeyResolver: typeof import("./models-config.providers.secrets.js").createProviderApiKeyResolver;
|
||||
let createProviderAuthResolver: typeof import("./models-config.providers.secrets.js").createProviderAuthResolver;
|
||||
let mockedResolveProviderSyntheticAuthWithPlugin: ReturnType<
|
||||
@@ -29,9 +29,10 @@ let mockedResolveProviderSyntheticAuthWithPlugin: ReturnType<
|
||||
async function loadProviderAuthModules() {
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
const [providerRuntimeModule, markersModule, secretsModule] = await Promise.all([
|
||||
const [providerRuntimeModule, markersModule, helperModule, secretsModule] = await Promise.all([
|
||||
import("../plugins/provider-runtime.js"),
|
||||
import("./model-auth-markers.js"),
|
||||
import("./models-config.providers.secret-helpers.js"),
|
||||
import("./models-config.providers.secrets.js"),
|
||||
]);
|
||||
mockedResolveProviderSyntheticAuthWithPlugin = vi.mocked(
|
||||
@@ -40,7 +41,7 @@ async function loadProviderAuthModules() {
|
||||
CUSTOM_LOCAL_AUTH_MARKER = markersModule.CUSTOM_LOCAL_AUTH_MARKER;
|
||||
NON_ENV_SECRETREF_MARKER = markersModule.NON_ENV_SECRETREF_MARKER;
|
||||
MINIMAX_OAUTH_MARKER = markersModule.MINIMAX_OAUTH_MARKER;
|
||||
resolveApiKeyFromCredential = secretsModule.resolveApiKeyFromCredential;
|
||||
resolveApiKeyFromCredential = helperModule.resolveApiKeyFromCredential;
|
||||
createProviderApiKeyResolver = secretsModule.createProviderApiKeyResolver;
|
||||
createProviderAuthResolver = secretsModule.createProviderAuthResolver;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export type SecretDefaults = {
|
||||
};
|
||||
|
||||
/** Resolved API key value plus provenance for discovery and secret-marker handling. */
|
||||
export type ProfileApiKeyResolution = {
|
||||
type ProfileApiKeyResolution = {
|
||||
apiKey: string;
|
||||
source: "plaintext" | "env-ref" | "non-env-ref";
|
||||
discoveryApiKey?: string;
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
|
||||
export type {
|
||||
ProfileApiKeyResolution,
|
||||
ProviderApiKeyResolver,
|
||||
ProviderAuthResolver,
|
||||
ProviderConfig,
|
||||
@@ -36,17 +35,8 @@ export type {
|
||||
} from "./models-config.providers.secret-helpers.js";
|
||||
|
||||
export {
|
||||
listAuthProfilesForProvider,
|
||||
normalizeApiKeyConfig,
|
||||
normalizeConfiguredProviderApiKey,
|
||||
normalizeHeaderValues,
|
||||
normalizeResolvedEnvApiKey,
|
||||
resolveApiKeyFromCredential,
|
||||
resolveApiKeyFromProfiles,
|
||||
resolveAwsSdkApiKeyVarName,
|
||||
resolveEnvApiKeyVarName,
|
||||
resolveMissingProviderApiKey,
|
||||
toDiscoveryApiKey,
|
||||
} from "./models-config.providers.secret-helpers.js";
|
||||
|
||||
type AuthProfileStoreInput = AuthProfileStore | (() => AuthProfileStore);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type BoundaryAllowedType = "file" | "directory";
|
||||
|
||||
/** Caller-provided path safety requirements for one fs bridge operation. */
|
||||
export type PathSafetyOptions = {
|
||||
type PathSafetyOptions = {
|
||||
action: string;
|
||||
aliasPolicy?: PathAliasPolicy;
|
||||
requireWritable?: boolean;
|
||||
|
||||
@@ -24,7 +24,7 @@ function stripWindowsNamespacePrefix(input: string): string {
|
||||
return input;
|
||||
}
|
||||
|
||||
export function isWindowsDriveAbsolutePath(raw: string): boolean {
|
||||
function isWindowsDriveAbsolutePath(raw: string): boolean {
|
||||
return /^[A-Za-z]:[\\/]/.test(stripWindowsNamespacePrefix(raw.trim()));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ type NoVncObserverTokenEntry = {
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
export type NoVncObserverTokenPayload = {
|
||||
type NoVncObserverTokenPayload = {
|
||||
noVncPort: number;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
@@ -41,10 +41,6 @@ export function resolveMaterializedSandboxSkillsWorkspaceDir(rootDir: string): s
|
||||
return path.join(rootDir, ...MATERIALIZED_SANDBOX_SKILLS_WORKSPACE_PARTS);
|
||||
}
|
||||
|
||||
export function resolveMaterializedSandboxSkillsRoot(rootDir: string): string {
|
||||
return path.join(resolveMaterializedSandboxSkillsWorkspaceDir(rootDir), "skills");
|
||||
}
|
||||
|
||||
/** Returns true when a skill mount source exists inside the canonical mount root. */
|
||||
export function isExistingWorkspaceSkillMountSource(params: {
|
||||
rootDir: string;
|
||||
|
||||
@@ -62,7 +62,7 @@ export function shouldDetachMediaGenerationTask(sessionKey: string | undefined):
|
||||
}
|
||||
|
||||
/** Successful media generation output used to complete and wake detached tasks. */
|
||||
export type MediaGenerationExecutionResult = {
|
||||
type MediaGenerationExecutionResult = {
|
||||
provider: string;
|
||||
model: string;
|
||||
count: number;
|
||||
|
||||
@@ -56,15 +56,6 @@ type ProgressExpectation = {
|
||||
progressSummary: string;
|
||||
};
|
||||
|
||||
type DirectSendExpectation = {
|
||||
sendMessageMock: unknown;
|
||||
channel: string;
|
||||
to: string;
|
||||
threadId: string;
|
||||
content: string;
|
||||
mediaUrls: string[];
|
||||
};
|
||||
|
||||
type FallbackAnnouncementExpectation = {
|
||||
deliverAnnouncementMock: unknown;
|
||||
requesterSessionKey: string;
|
||||
@@ -180,22 +171,6 @@ export function expectRecordedTaskProgress({
|
||||
expect(params.progressSummary).toBe(progressSummary);
|
||||
}
|
||||
|
||||
export function expectDirectMediaSend({
|
||||
sendMessageMock,
|
||||
channel,
|
||||
to,
|
||||
threadId,
|
||||
content,
|
||||
mediaUrls,
|
||||
}: DirectSendExpectation): void {
|
||||
const params = requireMockFirstParam(sendMessageMock, "sendMessage params");
|
||||
expect(params.channel).toBe(channel);
|
||||
expect(params.to).toBe(to);
|
||||
expect(params.threadId).toBe(threadId);
|
||||
expect(params.content).toBe(content);
|
||||
expect(params.mediaUrls).toEqual(mediaUrls);
|
||||
}
|
||||
|
||||
export function expectFallbackMediaAnnouncement({
|
||||
deliverAnnouncementMock,
|
||||
requesterSessionKey,
|
||||
|
||||
@@ -59,7 +59,7 @@ export const POLICY_REDIRECT_INVOKE_COMMANDS: ReadonlySet<string> = new Set([
|
||||
"file.write",
|
||||
]);
|
||||
|
||||
export type NodeMediaAction =
|
||||
type NodeMediaAction =
|
||||
| "camera_snap"
|
||||
| "photos_latest"
|
||||
| "camera_clip"
|
||||
|
||||
@@ -82,12 +82,6 @@ export const PdfToolSchema = Type.Object({
|
||||
maxBytesMb: optionalFiniteNumberSchema({ exclusiveMinimum: 0 }),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model resolution (mirrors image tool pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { resolvePdfModelConfigForTool } from "./pdf-tool.model-config.js";
|
||||
|
||||
function hasExplicitPdfToolModelConfig(config?: OpenClawConfig): boolean {
|
||||
return (
|
||||
hasToolModelConfig(coercePdfModelConfig(config)) ||
|
||||
|
||||
@@ -11,10 +11,8 @@ import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-r
|
||||
|
||||
export {
|
||||
createAgentToAgentPolicy,
|
||||
createSessionVisibilityChecker,
|
||||
createSessionVisibilityGuard,
|
||||
createSessionVisibilityRowChecker,
|
||||
listSpawnedSessionKeys,
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
} from "../../plugin-sdk/session-visibility.js";
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
|
||||
/** Coarse session category used by session list/status tools. */
|
||||
export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
|
||||
type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
|
||||
|
||||
/** Delivery target metadata attached to session rows. */
|
||||
export type SessionListDeliveryContext = {
|
||||
type SessionListDeliveryContext = {
|
||||
channel?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
|
||||
@@ -88,8 +88,6 @@ export function resolveCurrentSessionClientAlias(params: {
|
||||
return requesterKey;
|
||||
}
|
||||
|
||||
export { listSpawnedSessionKeys };
|
||||
|
||||
export async function isRequesterSpawnedSessionVisible(params: {
|
||||
requesterSessionKey: string;
|
||||
targetSessionKey: string;
|
||||
@@ -190,7 +188,7 @@ export function shouldResolveSessionIdInput(value: string): boolean {
|
||||
return looksLikeSessionId(value) || !looksLikeSessionKey(value);
|
||||
}
|
||||
|
||||
export type SessionReferenceResolution =
|
||||
type SessionReferenceResolution =
|
||||
| {
|
||||
ok: true;
|
||||
key: string;
|
||||
@@ -199,7 +197,7 @@ export type SessionReferenceResolution =
|
||||
}
|
||||
| { ok: false; status: "error" | "forbidden"; error: string };
|
||||
|
||||
export type VisibleSessionReferenceResolution =
|
||||
type VisibleSessionReferenceResolution =
|
||||
| {
|
||||
ok: true;
|
||||
key: string;
|
||||
@@ -508,8 +506,6 @@ export async function resolveVisibleSessionReference(params: {
|
||||
return { ok: true, key: resolvedKey, displayKey };
|
||||
}
|
||||
|
||||
export const normalizeOptionalKey: (value?: string) => string | undefined = normalizeOptionalString;
|
||||
|
||||
export const testing = {
|
||||
setDepsForTest(overrides?: Partial<{ callGateway: GatewayCaller }>) {
|
||||
sessionsResolutionDeps = overrides
|
||||
|
||||
55
test/scripts/pr-machine-output.test.ts
Normal file
55
test/scripts/pr-machine-output.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const PR_SCRIPT = path.resolve("scripts/pr");
|
||||
|
||||
describe("scripts/pr machine output", () => {
|
||||
it("disables inherited terminal color before calling gh", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-pr-machine-output-"));
|
||||
const binDir = path.join(root, "bin");
|
||||
const tracePath = path.join(root, "gh-env.txt");
|
||||
try {
|
||||
mkdirSync(binDir);
|
||||
const gitPath = path.join(binDir, "git");
|
||||
const ghPath = path.join(binDir, "gh");
|
||||
writeFileSync(gitPath, "#!/usr/bin/env bash\nexit 1\n", "utf8");
|
||||
writeFileSync(
|
||||
ghPath,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
'printf "NO_COLOR=%s CLICOLOR=%s CLICOLOR_FORCE=%s GH_FORCE_TTY=%s GH_PAGER=%s\\n" "$NO_COLOR" "$CLICOLOR" "$CLICOLOR_FORCE" "${GH_FORCE_TTY-<unset>}" "$GH_PAGER" > "$TRACE_PATH"',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
chmodSync(gitPath, 0o755);
|
||||
chmodSync(ghPath, 0o755);
|
||||
|
||||
const result = spawnSync("bash", [PR_SCRIPT, "review-init", "1"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CLICOLOR: "1",
|
||||
CLICOLOR_FORCE: "1",
|
||||
GH_FORCE_TTY: "1",
|
||||
GH_PAGER: "less",
|
||||
NO_COLOR: "0",
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
TRACE_PATH: tracePath,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(readFileSync(tracePath, "utf8")).toBe(
|
||||
"NO_COLOR=1 CLICOLOR=0 CLICOLOR_FORCE=0 GH_FORCE_TTY=<unset> GH_PAGER=cat\n",
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user