@clawling/clawchat-plugin-openclaw 2026.6.30-4 → 2026.7.2-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/api-client.js +13 -0
- package/dist/src/config.js +3 -0
- package/dist/src/friend-request-turn.js +73 -0
- package/dist/src/gate-outcome.js +26 -0
- package/dist/src/inbound.js +11 -0
- package/dist/src/permission-result.js +107 -0
- package/dist/src/permissions.js +6 -0
- package/dist/src/runtime.js +131 -9
- package/dist/src/skill-update.js +139 -69
- package/dist/src/tools.js +43 -10
- package/dist/src/ws-client.js +19 -6
- package/openclaw.plugin.json +2 -0
- package/package.json +1 -1
- package/src/api-client.ts +23 -0
- package/src/api-types.ts +1 -1
- package/src/config.ts +7 -0
- package/src/friend-request-turn.ts +87 -0
- package/src/gate-outcome.ts +44 -0
- package/src/inbound.ts +28 -0
- package/src/permission-result.ts +128 -0
- package/src/permissions.ts +13 -0
- package/src/runtime.ts +146 -10
- package/src/skill-update.ts +170 -85
- package/src/tools.ts +44 -10
- package/src/ws-client.ts +21 -6
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic reasoning-turn builder for `friend.request` notify.signal events.
|
|
3
|
+
*
|
|
4
|
+
* When the server fires a `friend.request` signal the runtime dispatches one
|
|
5
|
+
* deduped synthetic `message.send` envelope so the agent can reason about the
|
|
6
|
+
* pending request. The prompt text varies by the current "friend.add" permission
|
|
7
|
+
* policy state: a deny policy yields a decline/inform prompt; ask or allow
|
|
8
|
+
* yields a prompt that instructs the agent to review the request via the
|
|
9
|
+
* `clawchat_list_friend_requests` tool.
|
|
10
|
+
*/
|
|
11
|
+
import { EVENT, type Envelope } from "./protocol-types.ts";
|
|
12
|
+
import type { PermState } from "./permissions.ts";
|
|
13
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns canned prompt text for a friend-request reasoning turn based on the
|
|
17
|
+
* current "friend.add" permission policy state.
|
|
18
|
+
*
|
|
19
|
+
* - `deny` → inform the agent it should decline; no accept instruction.
|
|
20
|
+
* - `ask` / `allow` → instruct the agent to review the pending request.
|
|
21
|
+
*/
|
|
22
|
+
export function friendRequestPromptFor(state: PermState): string {
|
|
23
|
+
if (state === "deny") {
|
|
24
|
+
return [
|
|
25
|
+
"A new friend request has arrived.",
|
|
26
|
+
"Your current friend-add policy is set to deny.",
|
|
27
|
+
"Do not add this contact.",
|
|
28
|
+
"You may inform the requester that you cannot add them at this time.",
|
|
29
|
+
].join(" ");
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
"A new friend request has arrived.",
|
|
33
|
+
"Please review the pending request by calling `clawchat_list_friend_requests`",
|
|
34
|
+
"and decide whether to accept it.",
|
|
35
|
+
].join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BuildFriendRequestEnvelopeParams {
|
|
39
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
40
|
+
/** Current "friend.add" permission policy state. */
|
|
41
|
+
state: PermState;
|
|
42
|
+
/** entity_id from the notify.signal payload (the requesting user). */
|
|
43
|
+
entityId: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds a synthetic `message.send` envelope that triggers one agent reasoning
|
|
48
|
+
* turn for a pending friend request. The envelope shape mirrors the
|
|
49
|
+
* activation-bootstrap envelope; it targets the owner's direct conversation so
|
|
50
|
+
* the agent has context about who it is reasoning for.
|
|
51
|
+
*/
|
|
52
|
+
export function buildFriendRequestEnvelope(
|
|
53
|
+
params: BuildFriendRequestEnvelopeParams,
|
|
54
|
+
): Envelope {
|
|
55
|
+
const { account, state, entityId } = params;
|
|
56
|
+
const text = friendRequestPromptFor(state);
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
return {
|
|
59
|
+
version: "2",
|
|
60
|
+
event: EVENT.MESSAGE_SEND,
|
|
61
|
+
trace_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
|
|
62
|
+
emitted_at: now,
|
|
63
|
+
chat_id: account.ownerUserId,
|
|
64
|
+
chat_type: "direct",
|
|
65
|
+
to: { id: account.userId, type: "direct" },
|
|
66
|
+
sender: {
|
|
67
|
+
id: "clawchat-friend-request",
|
|
68
|
+
type: "direct",
|
|
69
|
+
nick_name: "ClawChat",
|
|
70
|
+
},
|
|
71
|
+
payload: {
|
|
72
|
+
message_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
|
|
73
|
+
message_mode: "normal",
|
|
74
|
+
message: {
|
|
75
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
76
|
+
context: { mentions: [], reply: null },
|
|
77
|
+
streaming: {
|
|
78
|
+
status: "static",
|
|
79
|
+
sequence: 0,
|
|
80
|
+
mutation_policy: "sealed",
|
|
81
|
+
started_at: null,
|
|
82
|
+
completed_at: null,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps backend owner-approval gate codes to structured tool outcomes.
|
|
3
|
+
*
|
|
4
|
+
* Backend gate codes (confirmed live contract):
|
|
5
|
+
* 21001 — pending owner approval (payload: { request_id, operation, expires_at })
|
|
6
|
+
* 21003 — policy forbidden (payload: { operation })
|
|
7
|
+
*
|
|
8
|
+
* Returns null for any other code so callers can fall through to normal handling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type PendingApprovalOutcome = {
|
|
12
|
+
status: "pending_owner_approval";
|
|
13
|
+
request_id: string;
|
|
14
|
+
operation: string;
|
|
15
|
+
expires_at: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ForbiddenOutcome = {
|
|
19
|
+
status: "forbidden_by_owner";
|
|
20
|
+
operation: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type GateOutcome = PendingApprovalOutcome | ForbiddenOutcome;
|
|
24
|
+
|
|
25
|
+
export function mapGateOutcome(
|
|
26
|
+
code: number,
|
|
27
|
+
data: Record<string, unknown>,
|
|
28
|
+
): GateOutcome | null {
|
|
29
|
+
if (code === 21001) {
|
|
30
|
+
return {
|
|
31
|
+
status: "pending_owner_approval",
|
|
32
|
+
request_id: typeof data.request_id === "string" ? data.request_id : "",
|
|
33
|
+
operation: typeof data.operation === "string" ? data.operation : "",
|
|
34
|
+
expires_at: typeof data.expires_at === "number" ? data.expires_at : 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (code === 21003) {
|
|
38
|
+
return {
|
|
39
|
+
status: "forbidden_by_owner",
|
|
40
|
+
operation: typeof data.operation === "string" ? data.operation : "",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
|
8
8
|
import type { MediaItem } from "./media-runtime.ts";
|
|
9
9
|
import { extractMediaFragments, fragmentsToText, type MentionedUser } from "./message-mapper.ts";
|
|
10
10
|
import { hasRenderableText, isInboundMessagePayload } from "./protocol.ts";
|
|
11
|
+
import type { PermissionResultMetadata } from "./permission-result.ts";
|
|
11
12
|
|
|
12
13
|
export interface IngestTurnParams {
|
|
13
14
|
channel: "clawchat-plugin-openclaw";
|
|
@@ -81,6 +82,20 @@ export interface DispatchInboundParams {
|
|
|
81
82
|
text: string;
|
|
82
83
|
chatId: string;
|
|
83
84
|
}) => Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Optional consumer for `permission_result` system-message receipts.
|
|
87
|
+
*
|
|
88
|
+
* When `sender.id === "system"` and `payload.metadata.kind ===
|
|
89
|
+
* "permission_result"`, the guard calls this callback with the metadata
|
|
90
|
+
* object instead of performing the generic system-message drop. The
|
|
91
|
+
* callback is responsible for deduplication and synthetic-turn dispatch
|
|
92
|
+
* (see `src/permission-result.ts`).
|
|
93
|
+
*
|
|
94
|
+
* When absent, `permission_result` frames fall through to the existing
|
|
95
|
+
* system-message drop (safe default — no regression for callers that have
|
|
96
|
+
* not yet wired the consumer).
|
|
97
|
+
*/
|
|
98
|
+
consumePermissionResult?: (metadata: PermissionResultMetadata) => Promise<void>;
|
|
84
99
|
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
85
100
|
}
|
|
86
101
|
|
|
@@ -241,6 +256,19 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
241
256
|
return;
|
|
242
257
|
}
|
|
243
258
|
if (sender.id === "system") {
|
|
259
|
+
// Intercept `permission_result` receipts before the generic system-message
|
|
260
|
+
// drop. The discriminator is `payload.metadata.kind === "permission_result"`;
|
|
261
|
+
// all other system messages (group-join notifications, etc.) still drop.
|
|
262
|
+
const rawPayload = envelope.payload as Record<string, unknown>;
|
|
263
|
+
const meta = rawPayload.metadata as Record<string, unknown> | undefined;
|
|
264
|
+
if (
|
|
265
|
+
params.consumePermissionResult &&
|
|
266
|
+
meta != null &&
|
|
267
|
+
meta.kind === "permission_result"
|
|
268
|
+
) {
|
|
269
|
+
await params.consumePermissionResult(meta as unknown as PermissionResultMetadata);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
244
272
|
log?.info?.(
|
|
245
273
|
`[${account.accountId}] clawchat-plugin-openclaw skip system message msg=${payload.message_id}`,
|
|
246
274
|
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer for `permission_result` system-message receipts.
|
|
3
|
+
*
|
|
4
|
+
* When the backend delivers a `permission_result` outcome as a
|
|
5
|
+
* `message.send` system frame (`sender.id="system"`, `payload.metadata.kind
|
|
6
|
+
* ="permission_result"`), this module deduplicates by `request_id` and
|
|
7
|
+
* produces a synthetic context-note turn so the agent can close the loop
|
|
8
|
+
* on the original permission request.
|
|
9
|
+
*
|
|
10
|
+
* Wire shape (authoritative):
|
|
11
|
+
* ```
|
|
12
|
+
* payload.metadata = {
|
|
13
|
+
* kind: "permission_result",
|
|
14
|
+
* operation: "friend.add",
|
|
15
|
+
* outcome: "approved" | "denied" | "expired" | "failed",
|
|
16
|
+
* reason: "owner_allowed" | …,
|
|
17
|
+
* request_id: "prq_…",
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
* The discriminator is `payload.metadata.kind === "permission_result"`.
|
|
21
|
+
*/
|
|
22
|
+
import { EVENT, type Envelope } from "./protocol-types.ts";
|
|
23
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
24
|
+
|
|
25
|
+
export interface PermissionResultMetadata {
|
|
26
|
+
kind: "permission_result";
|
|
27
|
+
operation: string;
|
|
28
|
+
outcome: "approved" | "denied" | "expired" | "failed";
|
|
29
|
+
reason: string;
|
|
30
|
+
request_id: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Module-level dedup set — `request_id` values are stable backend identifiers.
|
|
34
|
+
// A duplicate receipt (e.g. from WS replay on reconnect) must not re-trigger
|
|
35
|
+
// an agent turn.
|
|
36
|
+
const _seenRequestIds = new Set<string>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @internal Test helper — reset the dedup set between isolated test runs.
|
|
40
|
+
* Never call this in production code.
|
|
41
|
+
*/
|
|
42
|
+
export function _resetPermissionResultDedup(): void {
|
|
43
|
+
_seenRequestIds.clear();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildOutcomeNote(metadata: PermissionResultMetadata): string {
|
|
47
|
+
const { operation, outcome, reason } = metadata;
|
|
48
|
+
const outcomeLabel =
|
|
49
|
+
outcome === "approved"
|
|
50
|
+
? "approved"
|
|
51
|
+
: outcome === "denied"
|
|
52
|
+
? "denied"
|
|
53
|
+
: outcome === "expired"
|
|
54
|
+
? "expired"
|
|
55
|
+
: "failed";
|
|
56
|
+
const resolution =
|
|
57
|
+
outcome === "approved"
|
|
58
|
+
? "The requested action has been completed."
|
|
59
|
+
: "No further action was taken.";
|
|
60
|
+
return [
|
|
61
|
+
`Permission request for operation "${operation}" has been ${outcomeLabel}.`,
|
|
62
|
+
`Reason: ${reason}.`,
|
|
63
|
+
resolution,
|
|
64
|
+
].join(" ");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds a synthetic `message.send` envelope that delivers a
|
|
69
|
+
* `permission_result` context note to the agent.
|
|
70
|
+
*
|
|
71
|
+
* Mirrors the `buildFriendRequestEnvelope` pattern: a synthetic inbound
|
|
72
|
+
* `message.send` targeting the owner's direct conversation, routed through
|
|
73
|
+
* `handleInboundEnvelope` so the agent can reason about the outcome.
|
|
74
|
+
*/
|
|
75
|
+
export function buildPermissionResultEnvelope(params: {
|
|
76
|
+
account: Pick<ResolvedOpenclawClawlingAccount, "userId" | "ownerUserId">;
|
|
77
|
+
metadata: PermissionResultMetadata;
|
|
78
|
+
}): Envelope {
|
|
79
|
+
const { account, metadata } = params;
|
|
80
|
+
const text = buildOutcomeNote(metadata);
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
return {
|
|
83
|
+
version: "2",
|
|
84
|
+
event: EVENT.MESSAGE_SEND,
|
|
85
|
+
trace_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
|
|
86
|
+
emitted_at: now,
|
|
87
|
+
chat_id: account.ownerUserId,
|
|
88
|
+
chat_type: "direct",
|
|
89
|
+
to: { id: account.userId, type: "direct" },
|
|
90
|
+
sender: {
|
|
91
|
+
id: "clawchat-permission-result",
|
|
92
|
+
type: "direct",
|
|
93
|
+
nick_name: "ClawChat",
|
|
94
|
+
},
|
|
95
|
+
payload: {
|
|
96
|
+
message_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
|
|
97
|
+
message_mode: "normal",
|
|
98
|
+
message: {
|
|
99
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
100
|
+
context: { mentions: [], reply: null },
|
|
101
|
+
streaming: {
|
|
102
|
+
status: "static",
|
|
103
|
+
sequence: 0,
|
|
104
|
+
mutation_policy: "sealed",
|
|
105
|
+
started_at: null,
|
|
106
|
+
completed_at: null,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handles a `permission_result` receipt. Deduplicates by `request_id`
|
|
115
|
+
* (idempotent — WS replay on reconnect may deliver the same receipt again).
|
|
116
|
+
*
|
|
117
|
+
* Returns a synthetic context-note {@link Envelope} on a fresh receipt so
|
|
118
|
+
* the caller can feed it to `handleInboundEnvelope`; returns `null` when
|
|
119
|
+
* the `request_id` has already been processed.
|
|
120
|
+
*/
|
|
121
|
+
export function handlePermissionResult(
|
|
122
|
+
metadata: PermissionResultMetadata,
|
|
123
|
+
account: Pick<ResolvedOpenclawClawlingAccount, "userId" | "ownerUserId">,
|
|
124
|
+
): Envelope | null {
|
|
125
|
+
if (_seenRequestIds.has(metadata.request_id)) return null;
|
|
126
|
+
_seenRequestIds.add(metadata.request_id);
|
|
127
|
+
return buildPermissionResultEnvelope({ account, metadata });
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PermState = "ask" | "allow" | "deny";
|
|
2
|
+
|
|
3
|
+
export interface PermissionPolicy {
|
|
4
|
+
byOperation: Record<string, PermState>;
|
|
5
|
+
momentVisibility: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class PermissionCache {
|
|
9
|
+
private p: PermissionPolicy = { byOperation: {}, momentVisibility: [] };
|
|
10
|
+
get(): PermissionPolicy { return this.p; }
|
|
11
|
+
set(p: PermissionPolicy): void { this.p = p; }
|
|
12
|
+
stateOf(op: string): PermState { return this.p.byOperation[op] ?? "ask"; }
|
|
13
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -38,7 +38,8 @@ import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inboun
|
|
|
38
38
|
import {
|
|
39
39
|
PendingConsentStore,
|
|
40
40
|
handleOwnerConsentReply,
|
|
41
|
-
|
|
41
|
+
managedSkillExists,
|
|
42
|
+
readLocalSkillState,
|
|
42
43
|
resolveBundledSkillsDir,
|
|
43
44
|
resolveManagedSkillsDir,
|
|
44
45
|
runSkillUpdateCheck,
|
|
@@ -78,6 +79,9 @@ import {
|
|
|
78
79
|
import { createGroupMessageCoalescer } from "./group-message-coalescer.ts";
|
|
79
80
|
import { openclawLlmContextDebug } from "./llm-context-debug.ts";
|
|
80
81
|
import { GroupSettingsCache } from "./group-settings.ts";
|
|
82
|
+
import { PermissionCache } from "./permissions.ts";
|
|
83
|
+
import { buildFriendRequestEnvelope } from "./friend-request-turn.ts";
|
|
84
|
+
import { handlePermissionResult } from "./permission-result.ts";
|
|
81
85
|
|
|
82
86
|
type Log = { debug?: (m: string) => void; info?: (m: string) => void; error?: (m: string) => void };
|
|
83
87
|
type ChannelContextBuilder = (params: Record<string, unknown>) => unknown;
|
|
@@ -1752,6 +1756,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1752
1756
|
context: wsLogContext,
|
|
1753
1757
|
});
|
|
1754
1758
|
const groupSettingsCache = new GroupSettingsCache();
|
|
1759
|
+
const permissionCache = new PermissionCache();
|
|
1755
1760
|
// Findings 2 & 3 — gate group dispatch on the IN-FLIGHT settings pull, not just
|
|
1756
1761
|
// the first one. The pull is fire-and-forget, so replayed/live group messages
|
|
1757
1762
|
// could otherwise enter the governance gate against a stale/empty cache
|
|
@@ -1829,6 +1834,28 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1829
1834
|
}
|
|
1830
1835
|
})();
|
|
1831
1836
|
};
|
|
1837
|
+
// Best-effort pull of the agent's permission policy from the backend.
|
|
1838
|
+
// Called at startup (bypassRefresh: true, mirrors pullGroupSettings startup)
|
|
1839
|
+
// and on every successful (re)connect after hello-ok. On failure, logs and
|
|
1840
|
+
// keeps the stale cache — never throws, never blocks message handling.
|
|
1841
|
+
const pullPermissions = (opts?: { bypassRefresh?: boolean }): void => {
|
|
1842
|
+
void (async () => {
|
|
1843
|
+
try {
|
|
1844
|
+
const policy = opts?.bypassRefresh
|
|
1845
|
+
? await (conversationApiClient ??= buildConversationApiClient()).getMyPermissions()
|
|
1846
|
+
: await getConversationApiClient().getMyPermissions();
|
|
1847
|
+
permissionCache.set(policy);
|
|
1848
|
+
log?.info?.(
|
|
1849
|
+
`[${accountId}] clawchat-plugin-openclaw permissions pull ok ops=${Object.keys(policy.byOperation).length}`,
|
|
1850
|
+
);
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
log?.error?.(
|
|
1853
|
+
`[${accountId}] clawchat-plugin-openclaw permissions pull failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1854
|
+
);
|
|
1855
|
+
// Keep stale cache; never throw.
|
|
1856
|
+
}
|
|
1857
|
+
})();
|
|
1858
|
+
};
|
|
1832
1859
|
const logAuthFailure = (reason: string) => {
|
|
1833
1860
|
if (authFailureLogged) return;
|
|
1834
1861
|
authFailureLogged = true;
|
|
@@ -1962,6 +1989,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1962
1989
|
// Re-pull per-group agent settings on every successful (re)connect so the
|
|
1963
1990
|
// cache stays current after a network interruption or token refresh.
|
|
1964
1991
|
pullGroupSettings();
|
|
1992
|
+
// Re-pull permission policy alongside group settings on every (re)connect.
|
|
1993
|
+
pullPermissions();
|
|
1965
1994
|
// §A.1 — arm the proactive refresh timer from the live token's `exp`
|
|
1966
1995
|
// every time a connection becomes ready (re-armed after every refresh via
|
|
1967
1996
|
// the gateway re-enter).
|
|
@@ -2126,6 +2155,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2126
2155
|
// The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
|
|
2127
2156
|
// "current installed version" only — never written (it lives in node_modules
|
|
2128
2157
|
// → may be read-only and is clobbered on `openclaw plugins update`).
|
|
2158
|
+
// Update detection converges on raw sha256 (readLocalSkillState), not the
|
|
2159
|
+
// frontmatter version, so a same-version content fix still applies; a
|
|
2160
|
+
// manifest `removed` id tombstones the LOCAL managed copy (never bundled)
|
|
2161
|
+
// via removeManagedSkill, deleting only ids the official manifest names.
|
|
2129
2162
|
const skillUpdateConsent = new PendingConsentStore();
|
|
2130
2163
|
const managedSkillsDir = resolveManagedSkillsDir();
|
|
2131
2164
|
let bundledSkillsDir: string | null = null;
|
|
@@ -2138,13 +2171,30 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2138
2171
|
}`,
|
|
2139
2172
|
);
|
|
2140
2173
|
}
|
|
2141
|
-
|
|
2174
|
+
// A usr_... id is not a valid chat_id — member-backend rejects it with
|
|
2175
|
+
// "invalid conversation id" and msghub never acks the send. Proactive owner
|
|
2176
|
+
// messages must be addressed to the direct conversation recorded at
|
|
2177
|
+
// activation (parity with the hermes adapter's _owner_direct_chat_id).
|
|
2178
|
+
const resolveOwnerDirectConversationId = (): string | null => {
|
|
2142
2179
|
const ownerUserId = account.ownerUserId?.trim();
|
|
2143
|
-
if (!ownerUserId) return;
|
|
2180
|
+
if (!ownerUserId) return null;
|
|
2181
|
+
const conversationId = store
|
|
2182
|
+
?.getActivationConversation?.({ platform: "openclaw", accountId })
|
|
2183
|
+
?.conversationId?.trim();
|
|
2184
|
+
return conversationId || null;
|
|
2185
|
+
};
|
|
2186
|
+
const sendOwnerSkillMessage = async (text: string): Promise<void> => {
|
|
2187
|
+
const conversationId = resolveOwnerDirectConversationId();
|
|
2188
|
+
if (!conversationId) {
|
|
2189
|
+
log?.info?.(
|
|
2190
|
+
`[${accountId}] clawchat-plugin-openclaw skill-update owner message skipped: no activation conversation recorded`,
|
|
2191
|
+
);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2144
2194
|
await sendOpenclawClawlingText({
|
|
2145
2195
|
client,
|
|
2146
2196
|
account,
|
|
2147
|
-
to: { chatId:
|
|
2197
|
+
to: { chatId: conversationId, chatType: "direct" },
|
|
2148
2198
|
text,
|
|
2149
2199
|
log,
|
|
2150
2200
|
});
|
|
@@ -2158,10 +2208,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2158
2208
|
return;
|
|
2159
2209
|
}
|
|
2160
2210
|
// Effective current = managed copy if present, else bundled (host precedence).
|
|
2161
|
-
const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
|
|
2162
2211
|
await runSkillUpdateCheck({
|
|
2163
2212
|
ownerUserId,
|
|
2164
|
-
|
|
2213
|
+
readLocalState: (id) => readLocalSkillState(managedSkillsDir, bundledSkillsDir, id),
|
|
2214
|
+
hasManagedSkill: (id) => managedSkillExists(managedSkillsDir, id),
|
|
2165
2215
|
fetchFn: globalThis.fetch,
|
|
2166
2216
|
store: skillUpdateConsent,
|
|
2167
2217
|
sendOwnerMessage: sendOwnerSkillMessage,
|
|
@@ -2193,6 +2243,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2193
2243
|
});
|
|
2194
2244
|
};
|
|
2195
2245
|
|
|
2246
|
+
// Debounce flag for lighter-awareness consolidated notes.
|
|
2247
|
+
// A single queueMicrotask coalesces all awareness signals that arrive in
|
|
2248
|
+
// the same synchronous turn into exactly one handleInboundEnvelope call.
|
|
2249
|
+
let pendingAwarenessNote = false;
|
|
2250
|
+
|
|
2196
2251
|
client.on("notify:signal", (env: Envelope) => {
|
|
2197
2252
|
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
2198
2253
|
// cache (friends are fetched on demand via REST tools), so there is nothing
|
|
@@ -2204,11 +2259,17 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2204
2259
|
? (env.payload as Record<string, unknown>)
|
|
2205
2260
|
: undefined;
|
|
2206
2261
|
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
2262
|
+
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
2207
2263
|
if (type === "agent.config.changed") {
|
|
2208
2264
|
log?.info?.(
|
|
2209
2265
|
`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`,
|
|
2210
2266
|
);
|
|
2211
2267
|
pullGroupSettings();
|
|
2268
|
+
} else if (type === "agent.permission.changed") {
|
|
2269
|
+
log?.info?.(
|
|
2270
|
+
`[${accountId}] clawchat-plugin-openclaw agent.permission.changed signal received; re-pulling permissions`,
|
|
2271
|
+
);
|
|
2272
|
+
pullPermissions();
|
|
2212
2273
|
} else if (type === "clawchat.skill.update.check") {
|
|
2213
2274
|
// Content-free trigger: check the official skill source in code and, if
|
|
2214
2275
|
// there is an update, ask the owner in chat for consent. The atomic
|
|
@@ -2223,6 +2284,66 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2223
2284
|
}`,
|
|
2224
2285
|
);
|
|
2225
2286
|
});
|
|
2287
|
+
} else if (type === "friend.request") {
|
|
2288
|
+
// Dedup is already applied above by notifySignalObserver.observe (the live
|
|
2289
|
+
// frame and its reliable-inbox replay share the same event_id and collapse
|
|
2290
|
+
// to one observation). Build one synthetic reasoning turn so the agent can
|
|
2291
|
+
// act on the pending request; the prompt text varies by policy state.
|
|
2292
|
+
const state = permissionCache.stateOf("friend.add");
|
|
2293
|
+
log?.info?.(
|
|
2294
|
+
`[${accountId}] clawchat-plugin-openclaw friend.request signal received; dispatching reasoning turn entity_id=${entityId} policy=${state}`,
|
|
2295
|
+
);
|
|
2296
|
+
void handleInboundEnvelope(buildFriendRequestEnvelope({ account, state, entityId }));
|
|
2297
|
+
} else if (
|
|
2298
|
+
type === "friend.added" ||
|
|
2299
|
+
type === "friend.removed" ||
|
|
2300
|
+
type === "friend.profile_updated" ||
|
|
2301
|
+
type.startsWith("conversation.")
|
|
2302
|
+
) {
|
|
2303
|
+
// Lighter-awareness: log + optional single consolidated note. Never
|
|
2304
|
+
// injects a full reasoning turn per event (unlike friend.request).
|
|
2305
|
+
log?.info?.(
|
|
2306
|
+
`[${accountId}] clawchat-plugin-openclaw ${type} signal received; lighter-awareness handling`,
|
|
2307
|
+
);
|
|
2308
|
+
if (account.awarenessNote && !pendingAwarenessNote) {
|
|
2309
|
+
pendingAwarenessNote = true;
|
|
2310
|
+
queueMicrotask(() => {
|
|
2311
|
+
pendingAwarenessNote = false;
|
|
2312
|
+
const now = Date.now();
|
|
2313
|
+
void handleInboundEnvelope({
|
|
2314
|
+
version: "2",
|
|
2315
|
+
event: EVENT.MESSAGE_SEND,
|
|
2316
|
+
trace_id: `clawchat-plugin-openclaw-awareness-${now}`,
|
|
2317
|
+
emitted_at: now,
|
|
2318
|
+
chat_id: account.ownerUserId,
|
|
2319
|
+
chat_type: "direct",
|
|
2320
|
+
to: { id: account.userId, type: "direct" },
|
|
2321
|
+
sender: { id: "clawchat-awareness", type: "direct", nick_name: "ClawChat" },
|
|
2322
|
+
payload: {
|
|
2323
|
+
message_id: `clawchat-plugin-openclaw-awareness-${now}`,
|
|
2324
|
+
message_mode: "normal",
|
|
2325
|
+
message: {
|
|
2326
|
+
body: {
|
|
2327
|
+
fragments: [
|
|
2328
|
+
{
|
|
2329
|
+
kind: "text",
|
|
2330
|
+
text: "ClawChat awareness: your contacts or conversations have been updated.",
|
|
2331
|
+
},
|
|
2332
|
+
],
|
|
2333
|
+
},
|
|
2334
|
+
context: { mentions: [], reply: null },
|
|
2335
|
+
streaming: {
|
|
2336
|
+
status: "static",
|
|
2337
|
+
sequence: 0,
|
|
2338
|
+
mutation_policy: "sealed",
|
|
2339
|
+
started_at: null,
|
|
2340
|
+
completed_at: null,
|
|
2341
|
+
},
|
|
2342
|
+
},
|
|
2343
|
+
},
|
|
2344
|
+
} as unknown as Envelope);
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2226
2347
|
}
|
|
2227
2348
|
}
|
|
2228
2349
|
});
|
|
@@ -2787,10 +2908,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2787
2908
|
});
|
|
2788
2909
|
|
|
2789
2910
|
const forwardGroupApprovalSlashToOwner = async (turn: IngestTurnParams): Promise<IngestTurnResult> => {
|
|
2790
|
-
const
|
|
2791
|
-
if (!
|
|
2911
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
2912
|
+
if (!ownerConversationId) {
|
|
2792
2913
|
log?.error?.(
|
|
2793
|
-
`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=
|
|
2914
|
+
`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_conversation chat_id=${turn.peer.id} msg=${turn.messageId}`,
|
|
2794
2915
|
);
|
|
2795
2916
|
return "skipped";
|
|
2796
2917
|
}
|
|
@@ -2798,7 +2919,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2798
2919
|
await sendOpenclawClawlingText({
|
|
2799
2920
|
client,
|
|
2800
2921
|
account,
|
|
2801
|
-
to: { chatId:
|
|
2922
|
+
to: { chatId: ownerConversationId, chatType: "direct" },
|
|
2802
2923
|
text: groupOwnerAttentionText(turn.peer.id, turn.rawBody),
|
|
2803
2924
|
log,
|
|
2804
2925
|
});
|
|
@@ -2924,6 +3045,18 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2924
3045
|
account,
|
|
2925
3046
|
log,
|
|
2926
3047
|
consumeOwnerConsent: consumeOwnerSkillConsent,
|
|
3048
|
+
consumePermissionResult: async (metadata) => {
|
|
3049
|
+
// A permission_result receipt closes the loop on an earlier request.
|
|
3050
|
+
// Dedup by request_id; on a fresh receipt feed the synthetic context
|
|
3051
|
+
// note back to the agent so it can react to the outcome.
|
|
3052
|
+
const note = handlePermissionResult(metadata, account);
|
|
3053
|
+
if (note) {
|
|
3054
|
+
log?.info?.(
|
|
3055
|
+
`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`,
|
|
3056
|
+
);
|
|
3057
|
+
void handleInboundEnvelope(note);
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
2927
3060
|
ingest: async (turn) => {
|
|
2928
3061
|
ingestResult = await ingestTurn(turn);
|
|
2929
3062
|
},
|
|
@@ -3020,6 +3153,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3020
3153
|
// rotating `account.token` out from under the already-constructed WS client.
|
|
3021
3154
|
// Settling this releases the Finding-2 group-dispatch gate.
|
|
3022
3155
|
pullGroupSettings({ bypassRefresh: true });
|
|
3156
|
+
// Initial pull of permission policy at startup (best-effort; errors logged,
|
|
3157
|
+
// stale cache kept). Uses bypassRefresh for the same reason as pullGroupSettings.
|
|
3158
|
+
pullPermissions({ bypassRefresh: true });
|
|
3023
3159
|
// Safety: never strand a held group message if the runtime is torn down while a
|
|
3024
3160
|
// pull is in flight. `waitWithTimeout` already releases group waiters on abort,
|
|
3025
3161
|
// but force the gate open too so any later `wait()` resolves immediately.
|