@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
package/dist/src/api-client.js
CHANGED
|
@@ -196,6 +196,7 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
196
196
|
code,
|
|
197
197
|
status: res.status,
|
|
198
198
|
path,
|
|
199
|
+
...(env.data !== undefined ? { data: env.data } : {}),
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
202
|
return env.data;
|
|
@@ -516,5 +517,17 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
516
517
|
}
|
|
517
518
|
return { authoritative: true, rows };
|
|
518
519
|
},
|
|
520
|
+
async getMyPermissions() {
|
|
521
|
+
const data = await call("GET", "/v1/agents/me/permissions");
|
|
522
|
+
const byOperation = {};
|
|
523
|
+
let momentVisibility = [];
|
|
524
|
+
for (const [k, v] of Object.entries(data ?? {})) {
|
|
525
|
+
if (k === "moment.visibility")
|
|
526
|
+
momentVisibility = v ?? [];
|
|
527
|
+
else
|
|
528
|
+
byOperation[k] = v;
|
|
529
|
+
}
|
|
530
|
+
return { byOperation, momentVisibility };
|
|
531
|
+
},
|
|
519
532
|
};
|
|
520
533
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -81,6 +81,7 @@ export const openclawClawlingConfigSchema = {
|
|
|
81
81
|
forwardThinking: { type: "boolean" },
|
|
82
82
|
forwardToolCalls: { type: "boolean" },
|
|
83
83
|
richInteractions: { type: "boolean" },
|
|
84
|
+
awarenessNote: { type: "boolean" },
|
|
84
85
|
reconnect: {
|
|
85
86
|
type: "object",
|
|
86
87
|
additionalProperties: false,
|
|
@@ -328,6 +329,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
328
329
|
const forwardThinking = typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
329
330
|
const forwardToolCalls = typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
330
331
|
const richInteractions = typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
332
|
+
const awarenessNote = typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
|
|
331
333
|
return {
|
|
332
334
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
333
335
|
name: CHANNEL_ID,
|
|
@@ -353,6 +355,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
353
355
|
forwardThinking,
|
|
354
356
|
forwardToolCalls,
|
|
355
357
|
richInteractions,
|
|
358
|
+
awarenessNote,
|
|
356
359
|
allowFrom: [],
|
|
357
360
|
reconnect: readReconnect(channel.reconnect),
|
|
358
361
|
heartbeat: readHeartbeat(channel.heartbeat),
|
|
@@ -0,0 +1,73 @@
|
|
|
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 } from "./protocol-types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Returns canned prompt text for a friend-request reasoning turn based on the
|
|
14
|
+
* current "friend.add" permission policy state.
|
|
15
|
+
*
|
|
16
|
+
* - `deny` → inform the agent it should decline; no accept instruction.
|
|
17
|
+
* - `ask` / `allow` → instruct the agent to review the pending request.
|
|
18
|
+
*/
|
|
19
|
+
export function friendRequestPromptFor(state) {
|
|
20
|
+
if (state === "deny") {
|
|
21
|
+
return [
|
|
22
|
+
"A new friend request has arrived.",
|
|
23
|
+
"Your current friend-add policy is set to deny.",
|
|
24
|
+
"Do not add this contact.",
|
|
25
|
+
"You may inform the requester that you cannot add them at this time.",
|
|
26
|
+
].join(" ");
|
|
27
|
+
}
|
|
28
|
+
return [
|
|
29
|
+
"A new friend request has arrived.",
|
|
30
|
+
"Please review the pending request by calling `clawchat_list_friend_requests`",
|
|
31
|
+
"and decide whether to accept it.",
|
|
32
|
+
].join(" ");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds a synthetic `message.send` envelope that triggers one agent reasoning
|
|
36
|
+
* turn for a pending friend request. The envelope shape mirrors the
|
|
37
|
+
* activation-bootstrap envelope; it targets the owner's direct conversation so
|
|
38
|
+
* the agent has context about who it is reasoning for.
|
|
39
|
+
*/
|
|
40
|
+
export function buildFriendRequestEnvelope(params) {
|
|
41
|
+
const { account, state, entityId } = params;
|
|
42
|
+
const text = friendRequestPromptFor(state);
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
return {
|
|
45
|
+
version: "2",
|
|
46
|
+
event: EVENT.MESSAGE_SEND,
|
|
47
|
+
trace_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
|
|
48
|
+
emitted_at: now,
|
|
49
|
+
chat_id: account.ownerUserId,
|
|
50
|
+
chat_type: "direct",
|
|
51
|
+
to: { id: account.userId, type: "direct" },
|
|
52
|
+
sender: {
|
|
53
|
+
id: "clawchat-friend-request",
|
|
54
|
+
type: "direct",
|
|
55
|
+
nick_name: "ClawChat",
|
|
56
|
+
},
|
|
57
|
+
payload: {
|
|
58
|
+
message_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
|
|
59
|
+
message_mode: "normal",
|
|
60
|
+
message: {
|
|
61
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
62
|
+
context: { mentions: [], reply: null },
|
|
63
|
+
streaming: {
|
|
64
|
+
status: "static",
|
|
65
|
+
sequence: 0,
|
|
66
|
+
mutation_policy: "sealed",
|
|
67
|
+
started_at: null,
|
|
68
|
+
completed_at: null,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
export function mapGateOutcome(code, data) {
|
|
11
|
+
if (code === 21001) {
|
|
12
|
+
return {
|
|
13
|
+
status: "pending_owner_approval",
|
|
14
|
+
request_id: typeof data.request_id === "string" ? data.request_id : "",
|
|
15
|
+
operation: typeof data.operation === "string" ? data.operation : "",
|
|
16
|
+
expires_at: typeof data.expires_at === "number" ? data.expires_at : 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (code === 21003) {
|
|
20
|
+
return {
|
|
21
|
+
status: "forbidden_by_owner",
|
|
22
|
+
operation: typeof data.operation === "string" ? data.operation : "",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
package/dist/src/inbound.js
CHANGED
|
@@ -115,6 +115,17 @@ export async function dispatchOpenclawClawlingInbound(params) {
|
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
if (sender.id === "system") {
|
|
118
|
+
// Intercept `permission_result` receipts before the generic system-message
|
|
119
|
+
// drop. The discriminator is `payload.metadata.kind === "permission_result"`;
|
|
120
|
+
// all other system messages (group-join notifications, etc.) still drop.
|
|
121
|
+
const rawPayload = envelope.payload;
|
|
122
|
+
const meta = rawPayload.metadata;
|
|
123
|
+
if (params.consumePermissionResult &&
|
|
124
|
+
meta != null &&
|
|
125
|
+
meta.kind === "permission_result") {
|
|
126
|
+
await params.consumePermissionResult(meta);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
118
129
|
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw skip system message msg=${payload.message_id}`);
|
|
119
130
|
return;
|
|
120
131
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 } from "./protocol-types.js";
|
|
23
|
+
// Module-level dedup set — `request_id` values are stable backend identifiers.
|
|
24
|
+
// A duplicate receipt (e.g. from WS replay on reconnect) must not re-trigger
|
|
25
|
+
// an agent turn.
|
|
26
|
+
const _seenRequestIds = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* @internal Test helper — reset the dedup set between isolated test runs.
|
|
29
|
+
* Never call this in production code.
|
|
30
|
+
*/
|
|
31
|
+
export function _resetPermissionResultDedup() {
|
|
32
|
+
_seenRequestIds.clear();
|
|
33
|
+
}
|
|
34
|
+
function buildOutcomeNote(metadata) {
|
|
35
|
+
const { operation, outcome, reason } = metadata;
|
|
36
|
+
const outcomeLabel = outcome === "approved"
|
|
37
|
+
? "approved"
|
|
38
|
+
: outcome === "denied"
|
|
39
|
+
? "denied"
|
|
40
|
+
: outcome === "expired"
|
|
41
|
+
? "expired"
|
|
42
|
+
: "failed";
|
|
43
|
+
const resolution = outcome === "approved"
|
|
44
|
+
? "The requested action has been completed."
|
|
45
|
+
: "No further action was taken.";
|
|
46
|
+
return [
|
|
47
|
+
`Permission request for operation "${operation}" has been ${outcomeLabel}.`,
|
|
48
|
+
`Reason: ${reason}.`,
|
|
49
|
+
resolution,
|
|
50
|
+
].join(" ");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds a synthetic `message.send` envelope that delivers a
|
|
54
|
+
* `permission_result` context note to the agent.
|
|
55
|
+
*
|
|
56
|
+
* Mirrors the `buildFriendRequestEnvelope` pattern: a synthetic inbound
|
|
57
|
+
* `message.send` targeting the owner's direct conversation, routed through
|
|
58
|
+
* `handleInboundEnvelope` so the agent can reason about the outcome.
|
|
59
|
+
*/
|
|
60
|
+
export function buildPermissionResultEnvelope(params) {
|
|
61
|
+
const { account, metadata } = params;
|
|
62
|
+
const text = buildOutcomeNote(metadata);
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
return {
|
|
65
|
+
version: "2",
|
|
66
|
+
event: EVENT.MESSAGE_SEND,
|
|
67
|
+
trace_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
|
|
68
|
+
emitted_at: now,
|
|
69
|
+
chat_id: account.ownerUserId,
|
|
70
|
+
chat_type: "direct",
|
|
71
|
+
to: { id: account.userId, type: "direct" },
|
|
72
|
+
sender: {
|
|
73
|
+
id: "clawchat-permission-result",
|
|
74
|
+
type: "direct",
|
|
75
|
+
nick_name: "ClawChat",
|
|
76
|
+
},
|
|
77
|
+
payload: {
|
|
78
|
+
message_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
|
|
79
|
+
message_mode: "normal",
|
|
80
|
+
message: {
|
|
81
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
82
|
+
context: { mentions: [], reply: null },
|
|
83
|
+
streaming: {
|
|
84
|
+
status: "static",
|
|
85
|
+
sequence: 0,
|
|
86
|
+
mutation_policy: "sealed",
|
|
87
|
+
started_at: null,
|
|
88
|
+
completed_at: null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Handles a `permission_result` receipt. Deduplicates by `request_id`
|
|
96
|
+
* (idempotent — WS replay on reconnect may deliver the same receipt again).
|
|
97
|
+
*
|
|
98
|
+
* Returns a synthetic context-note {@link Envelope} on a fresh receipt so
|
|
99
|
+
* the caller can feed it to `handleInboundEnvelope`; returns `null` when
|
|
100
|
+
* the `request_id` has already been processed.
|
|
101
|
+
*/
|
|
102
|
+
export function handlePermissionResult(metadata, account) {
|
|
103
|
+
if (_seenRequestIds.has(metadata.request_id))
|
|
104
|
+
return null;
|
|
105
|
+
_seenRequestIds.add(metadata.request_id);
|
|
106
|
+
return buildPermissionResultEnvelope({ account, metadata });
|
|
107
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -11,7 +11,7 @@ import { RefreshManager } from "./refresh-manager.js";
|
|
|
11
11
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
12
12
|
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
13
13
|
import { dispatchOpenclawClawlingInbound } from "./inbound.js";
|
|
14
|
-
import { PendingConsentStore, handleOwnerConsentReply,
|
|
14
|
+
import { PendingConsentStore, handleOwnerConsentReply, managedSkillExists, readLocalSkillState, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
|
|
15
15
|
import { fetchInboundMedia } from "./media-runtime.js";
|
|
16
16
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
|
|
17
17
|
import { runWithTerminalClawChatSendScope } from "./terminal-send.js";
|
|
@@ -28,6 +28,9 @@ import { clearClawChatPromptInjectionForSession, stageClawChatPromptInjection, }
|
|
|
28
28
|
import { createGroupMessageCoalescer } from "./group-message-coalescer.js";
|
|
29
29
|
import { openclawLlmContextDebug } from "./llm-context-debug.js";
|
|
30
30
|
import { GroupSettingsCache } from "./group-settings.js";
|
|
31
|
+
import { PermissionCache } from "./permissions.js";
|
|
32
|
+
import { buildFriendRequestEnvelope } from "./friend-request-turn.js";
|
|
33
|
+
import { handlePermissionResult } from "./permission-result.js";
|
|
31
34
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } = createPluginRuntimeStore("clawchat-plugin-openclaw runtime not initialized");
|
|
32
35
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
33
36
|
const activeClients = new Map();
|
|
@@ -1381,6 +1384,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1381
1384
|
context: wsLogContext,
|
|
1382
1385
|
});
|
|
1383
1386
|
const groupSettingsCache = new GroupSettingsCache();
|
|
1387
|
+
const permissionCache = new PermissionCache();
|
|
1384
1388
|
// Findings 2 & 3 — gate group dispatch on the IN-FLIGHT settings pull, not just
|
|
1385
1389
|
// the first one. The pull is fire-and-forget, so replayed/live group messages
|
|
1386
1390
|
// could otherwise enter the governance gate against a stale/empty cache
|
|
@@ -1454,6 +1458,25 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1454
1458
|
}
|
|
1455
1459
|
})();
|
|
1456
1460
|
};
|
|
1461
|
+
// Best-effort pull of the agent's permission policy from the backend.
|
|
1462
|
+
// Called at startup (bypassRefresh: true, mirrors pullGroupSettings startup)
|
|
1463
|
+
// and on every successful (re)connect after hello-ok. On failure, logs and
|
|
1464
|
+
// keeps the stale cache — never throws, never blocks message handling.
|
|
1465
|
+
const pullPermissions = (opts) => {
|
|
1466
|
+
void (async () => {
|
|
1467
|
+
try {
|
|
1468
|
+
const policy = opts?.bypassRefresh
|
|
1469
|
+
? await (conversationApiClient ??= buildConversationApiClient()).getMyPermissions()
|
|
1470
|
+
: await getConversationApiClient().getMyPermissions();
|
|
1471
|
+
permissionCache.set(policy);
|
|
1472
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw permissions pull ok ops=${Object.keys(policy.byOperation).length}`);
|
|
1473
|
+
}
|
|
1474
|
+
catch (err) {
|
|
1475
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw permissions pull failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1476
|
+
// Keep stale cache; never throw.
|
|
1477
|
+
}
|
|
1478
|
+
})();
|
|
1479
|
+
};
|
|
1457
1480
|
const logAuthFailure = (reason) => {
|
|
1458
1481
|
if (authFailureLogged)
|
|
1459
1482
|
return;
|
|
@@ -1585,6 +1608,8 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1585
1608
|
// Re-pull per-group agent settings on every successful (re)connect so the
|
|
1586
1609
|
// cache stays current after a network interruption or token refresh.
|
|
1587
1610
|
pullGroupSettings();
|
|
1611
|
+
// Re-pull permission policy alongside group settings on every (re)connect.
|
|
1612
|
+
pullPermissions();
|
|
1588
1613
|
// §A.1 — arm the proactive refresh timer from the live token's `exp`
|
|
1589
1614
|
// every time a connection becomes ready (re-armed after every refresh via
|
|
1590
1615
|
// the gateway re-enter).
|
|
@@ -1737,6 +1762,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1737
1762
|
// The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
|
|
1738
1763
|
// "current installed version" only — never written (it lives in node_modules
|
|
1739
1764
|
// → may be read-only and is clobbered on `openclaw plugins update`).
|
|
1765
|
+
// Update detection converges on raw sha256 (readLocalSkillState), not the
|
|
1766
|
+
// frontmatter version, so a same-version content fix still applies; a
|
|
1767
|
+
// manifest `removed` id tombstones the LOCAL managed copy (never bundled)
|
|
1768
|
+
// via removeManagedSkill, deleting only ids the official manifest names.
|
|
1740
1769
|
const skillUpdateConsent = new PendingConsentStore();
|
|
1741
1770
|
const managedSkillsDir = resolveManagedSkillsDir();
|
|
1742
1771
|
let bundledSkillsDir = null;
|
|
@@ -1746,14 +1775,29 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1746
1775
|
catch (err) {
|
|
1747
1776
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update: bundled skills dir not resolvable (using managed-only current): ${err instanceof Error ? err.message : String(err)}`);
|
|
1748
1777
|
}
|
|
1749
|
-
|
|
1778
|
+
// A usr_... id is not a valid chat_id — member-backend rejects it with
|
|
1779
|
+
// "invalid conversation id" and msghub never acks the send. Proactive owner
|
|
1780
|
+
// messages must be addressed to the direct conversation recorded at
|
|
1781
|
+
// activation (parity with the hermes adapter's _owner_direct_chat_id).
|
|
1782
|
+
const resolveOwnerDirectConversationId = () => {
|
|
1750
1783
|
const ownerUserId = account.ownerUserId?.trim();
|
|
1751
1784
|
if (!ownerUserId)
|
|
1785
|
+
return null;
|
|
1786
|
+
const conversationId = store
|
|
1787
|
+
?.getActivationConversation?.({ platform: "openclaw", accountId })
|
|
1788
|
+
?.conversationId?.trim();
|
|
1789
|
+
return conversationId || null;
|
|
1790
|
+
};
|
|
1791
|
+
const sendOwnerSkillMessage = async (text) => {
|
|
1792
|
+
const conversationId = resolveOwnerDirectConversationId();
|
|
1793
|
+
if (!conversationId) {
|
|
1794
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update owner message skipped: no activation conversation recorded`);
|
|
1752
1795
|
return;
|
|
1796
|
+
}
|
|
1753
1797
|
await sendOpenclawClawlingText({
|
|
1754
1798
|
client,
|
|
1755
1799
|
account,
|
|
1756
|
-
to: { chatId:
|
|
1800
|
+
to: { chatId: conversationId, chatType: "direct" },
|
|
1757
1801
|
text,
|
|
1758
1802
|
log,
|
|
1759
1803
|
});
|
|
@@ -1765,10 +1809,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1765
1809
|
return;
|
|
1766
1810
|
}
|
|
1767
1811
|
// Effective current = managed copy if present, else bundled (host precedence).
|
|
1768
|
-
const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
|
|
1769
1812
|
await runSkillUpdateCheck({
|
|
1770
1813
|
ownerUserId,
|
|
1771
|
-
|
|
1814
|
+
readLocalState: (id) => readLocalSkillState(managedSkillsDir, bundledSkillsDir, id),
|
|
1815
|
+
hasManagedSkill: (id) => managedSkillExists(managedSkillsDir, id),
|
|
1772
1816
|
fetchFn: globalThis.fetch,
|
|
1773
1817
|
store: skillUpdateConsent,
|
|
1774
1818
|
sendOwnerMessage: sendOwnerSkillMessage,
|
|
@@ -1796,6 +1840,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1796
1840
|
},
|
|
1797
1841
|
});
|
|
1798
1842
|
};
|
|
1843
|
+
// Debounce flag for lighter-awareness consolidated notes.
|
|
1844
|
+
// A single queueMicrotask coalesces all awareness signals that arrive in
|
|
1845
|
+
// the same synchronous turn into exactly one handleInboundEnvelope call.
|
|
1846
|
+
let pendingAwarenessNote = false;
|
|
1799
1847
|
client.on("notify:signal", (env) => {
|
|
1800
1848
|
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
1801
1849
|
// cache (friends are fetched on demand via REST tools), so there is nothing
|
|
@@ -1807,10 +1855,15 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1807
1855
|
? env.payload
|
|
1808
1856
|
: undefined;
|
|
1809
1857
|
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
1858
|
+
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
1810
1859
|
if (type === "agent.config.changed") {
|
|
1811
1860
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`);
|
|
1812
1861
|
pullGroupSettings();
|
|
1813
1862
|
}
|
|
1863
|
+
else if (type === "agent.permission.changed") {
|
|
1864
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw agent.permission.changed signal received; re-pulling permissions`);
|
|
1865
|
+
pullPermissions();
|
|
1866
|
+
}
|
|
1814
1867
|
else if (type === "clawchat.skill.update.check") {
|
|
1815
1868
|
// Content-free trigger: check the official skill source in code and, if
|
|
1816
1869
|
// there is an update, ask the owner in chat for consent. The atomic
|
|
@@ -1820,6 +1873,62 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1820
1873
|
log?.error?.(`[${accountId}] clawchat-plugin-openclaw skill-update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1821
1874
|
});
|
|
1822
1875
|
}
|
|
1876
|
+
else if (type === "friend.request") {
|
|
1877
|
+
// Dedup is already applied above by notifySignalObserver.observe (the live
|
|
1878
|
+
// frame and its reliable-inbox replay share the same event_id and collapse
|
|
1879
|
+
// to one observation). Build one synthetic reasoning turn so the agent can
|
|
1880
|
+
// act on the pending request; the prompt text varies by policy state.
|
|
1881
|
+
const state = permissionCache.stateOf("friend.add");
|
|
1882
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw friend.request signal received; dispatching reasoning turn entity_id=${entityId} policy=${state}`);
|
|
1883
|
+
void handleInboundEnvelope(buildFriendRequestEnvelope({ account, state, entityId }));
|
|
1884
|
+
}
|
|
1885
|
+
else if (type === "friend.added" ||
|
|
1886
|
+
type === "friend.removed" ||
|
|
1887
|
+
type === "friend.profile_updated" ||
|
|
1888
|
+
type.startsWith("conversation.")) {
|
|
1889
|
+
// Lighter-awareness: log + optional single consolidated note. Never
|
|
1890
|
+
// injects a full reasoning turn per event (unlike friend.request).
|
|
1891
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${type} signal received; lighter-awareness handling`);
|
|
1892
|
+
if (account.awarenessNote && !pendingAwarenessNote) {
|
|
1893
|
+
pendingAwarenessNote = true;
|
|
1894
|
+
queueMicrotask(() => {
|
|
1895
|
+
pendingAwarenessNote = false;
|
|
1896
|
+
const now = Date.now();
|
|
1897
|
+
void handleInboundEnvelope({
|
|
1898
|
+
version: "2",
|
|
1899
|
+
event: EVENT.MESSAGE_SEND,
|
|
1900
|
+
trace_id: `clawchat-plugin-openclaw-awareness-${now}`,
|
|
1901
|
+
emitted_at: now,
|
|
1902
|
+
chat_id: account.ownerUserId,
|
|
1903
|
+
chat_type: "direct",
|
|
1904
|
+
to: { id: account.userId, type: "direct" },
|
|
1905
|
+
sender: { id: "clawchat-awareness", type: "direct", nick_name: "ClawChat" },
|
|
1906
|
+
payload: {
|
|
1907
|
+
message_id: `clawchat-plugin-openclaw-awareness-${now}`,
|
|
1908
|
+
message_mode: "normal",
|
|
1909
|
+
message: {
|
|
1910
|
+
body: {
|
|
1911
|
+
fragments: [
|
|
1912
|
+
{
|
|
1913
|
+
kind: "text",
|
|
1914
|
+
text: "ClawChat awareness: your contacts or conversations have been updated.",
|
|
1915
|
+
},
|
|
1916
|
+
],
|
|
1917
|
+
},
|
|
1918
|
+
context: { mentions: [], reply: null },
|
|
1919
|
+
streaming: {
|
|
1920
|
+
status: "static",
|
|
1921
|
+
sequence: 0,
|
|
1922
|
+
mutation_policy: "sealed",
|
|
1923
|
+
started_at: null,
|
|
1924
|
+
completed_at: null,
|
|
1925
|
+
},
|
|
1926
|
+
},
|
|
1927
|
+
},
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1823
1932
|
}
|
|
1824
1933
|
});
|
|
1825
1934
|
client.on("replay:done", (env) => {
|
|
@@ -2318,16 +2427,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2318
2427
|
},
|
|
2319
2428
|
});
|
|
2320
2429
|
const forwardGroupApprovalSlashToOwner = async (turn) => {
|
|
2321
|
-
const
|
|
2322
|
-
if (!
|
|
2323
|
-
log?.error?.(`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=
|
|
2430
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
2431
|
+
if (!ownerConversationId) {
|
|
2432
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_conversation chat_id=${turn.peer.id} msg=${turn.messageId}`);
|
|
2324
2433
|
return "skipped";
|
|
2325
2434
|
}
|
|
2326
2435
|
try {
|
|
2327
2436
|
await sendOpenclawClawlingText({
|
|
2328
2437
|
client,
|
|
2329
2438
|
account,
|
|
2330
|
-
to: { chatId:
|
|
2439
|
+
to: { chatId: ownerConversationId, chatType: "direct" },
|
|
2331
2440
|
text: groupOwnerAttentionText(turn.peer.id, turn.rawBody),
|
|
2332
2441
|
log,
|
|
2333
2442
|
});
|
|
@@ -2439,6 +2548,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2439
2548
|
account,
|
|
2440
2549
|
log,
|
|
2441
2550
|
consumeOwnerConsent: consumeOwnerSkillConsent,
|
|
2551
|
+
consumePermissionResult: async (metadata) => {
|
|
2552
|
+
// A permission_result receipt closes the loop on an earlier request.
|
|
2553
|
+
// Dedup by request_id; on a fresh receipt feed the synthetic context
|
|
2554
|
+
// note back to the agent so it can react to the outcome.
|
|
2555
|
+
const note = handlePermissionResult(metadata, account);
|
|
2556
|
+
if (note) {
|
|
2557
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`);
|
|
2558
|
+
void handleInboundEnvelope(note);
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2442
2561
|
ingest: async (turn) => {
|
|
2443
2562
|
ingestResult = await ingestTurn(turn);
|
|
2444
2563
|
},
|
|
@@ -2525,6 +2644,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2525
2644
|
// rotating `account.token` out from under the already-constructed WS client.
|
|
2526
2645
|
// Settling this releases the Finding-2 group-dispatch gate.
|
|
2527
2646
|
pullGroupSettings({ bypassRefresh: true });
|
|
2647
|
+
// Initial pull of permission policy at startup (best-effort; errors logged,
|
|
2648
|
+
// stale cache kept). Uses bypassRefresh for the same reason as pullGroupSettings.
|
|
2649
|
+
pullPermissions({ bypassRefresh: true });
|
|
2528
2650
|
// Safety: never strand a held group message if the runtime is torn down while a
|
|
2529
2651
|
// pull is in flight. `waitWithTimeout` already releases group waiters on abort,
|
|
2530
2652
|
// but force the gate open too so any later `wait()` resolves immediately.
|