@aiassesstech/nole 0.3.7 → 0.4.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/agent/AGENTS.md +34 -0
- package/dist/governance/types.d.ts.map +1 -1
- package/dist/governance/types.js +2 -0
- package/dist/governance/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +781 -25
- package/dist/plugin.js.map +1 -1
- package/dist/social/linkedin-drafter.d.ts +16 -0
- package/dist/social/linkedin-drafter.d.ts.map +1 -0
- package/dist/social/linkedin-drafter.js +66 -0
- package/dist/social/linkedin-drafter.js.map +1 -0
- package/dist/social/moltbook-client.d.ts +28 -0
- package/dist/social/moltbook-client.d.ts.map +1 -0
- package/dist/social/moltbook-client.js +97 -0
- package/dist/social/moltbook-client.js.map +1 -0
- package/dist/social/rate-limiter.d.ts +22 -0
- package/dist/social/rate-limiter.d.ts.map +1 -0
- package/dist/social/rate-limiter.js +67 -0
- package/dist/social/rate-limiter.js.map +1 -0
- package/dist/social/types.d.ts +99 -0
- package/dist/social/types.d.ts.map +1 -0
- package/dist/social/types.js +6 -0
- package/dist/social/types.js.map +1 -0
- package/dist/social/x-client.d.ts +33 -0
- package/dist/social/x-client.d.ts.map +1 -0
- package/dist/social/x-client.js +167 -0
- package/dist/social/x-client.js.map +1 -0
- package/dist/store/json-store.d.ts +3 -0
- package/dist/store/json-store.d.ts.map +1 -1
- package/dist/store/json-store.js +24 -0
- package/dist/store/json-store.js.map +1 -1
- package/dist/store/types.d.ts +3 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/types/nole-config.d.ts +24 -0
- package/dist/types/nole-config.d.ts.map +1 -1
- package/dist/types/nole-config.js +9 -0
- package/dist/types/nole-config.js.map +1 -1
- package/openclaw.plugin.json +36 -0
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -11,6 +11,10 @@ import { parseNoleConfig } from './types/nole-config.js';
|
|
|
11
11
|
import { NOLE_IDENTITY } from './types/identity.js';
|
|
12
12
|
import { PlatformApiClient } from './compsi/api-client.js';
|
|
13
13
|
import { createWalletAdapter, MockWalletAdapter } from './wallet/index.js';
|
|
14
|
+
import { MoltBookClient } from './social/moltbook-client.js';
|
|
15
|
+
import { XClient } from './social/x-client.js';
|
|
16
|
+
import { LinkedInDrafter } from './social/linkedin-drafter.js';
|
|
17
|
+
import { createMoltBookLimiter, createXLimiter } from './social/rate-limiter.js';
|
|
14
18
|
/**
|
|
15
19
|
* OpenClaw plugin entry point — export default function register(api)
|
|
16
20
|
*
|
|
@@ -101,6 +105,182 @@ export default function register(api) {
|
|
|
101
105
|
console.error(`[nole] Wallet init failed (using mock): ${errMsg}`);
|
|
102
106
|
}
|
|
103
107
|
})();
|
|
108
|
+
// ── Social Media Clients (nullable — checked at call time per rule 155) ──
|
|
109
|
+
const moltbookLimiter = createMoltBookLimiter();
|
|
110
|
+
const xLimiter = createXLimiter();
|
|
111
|
+
const moltbook = config.moltbookApiKey
|
|
112
|
+
? new MoltBookClient({ apiKey: config.moltbookApiKey, baseUrl: config.moltbookBaseUrl }, moltbookLimiter)
|
|
113
|
+
: null;
|
|
114
|
+
const xClient = config.xApiKey && config.xApiSecret && config.xAccessToken && config.xAccessTokenSecret
|
|
115
|
+
? new XClient({
|
|
116
|
+
apiKey: config.xApiKey,
|
|
117
|
+
apiSecret: config.xApiSecret,
|
|
118
|
+
accessToken: config.xAccessToken,
|
|
119
|
+
accessTokenSecret: config.xAccessTokenSecret,
|
|
120
|
+
handle: config.xHandle,
|
|
121
|
+
}, xLimiter)
|
|
122
|
+
: null;
|
|
123
|
+
const linkedInDrafter = new LinkedInDrafter(dataDir);
|
|
124
|
+
linkedInDrafter.initialize().catch((err) => {
|
|
125
|
+
console.warn(`[nole] LinkedIn drafter init warning: ${err.message}`);
|
|
126
|
+
});
|
|
127
|
+
const generateSocialId = () => `SOC-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
128
|
+
let socialShield = null;
|
|
129
|
+
let outputSanitizer = null;
|
|
130
|
+
let fleetShieldMiddleware = null;
|
|
131
|
+
let validatedToolFactory = null;
|
|
132
|
+
let validationSchemas = {};
|
|
133
|
+
let validationBusinessRules = {};
|
|
134
|
+
const promptShieldMod = '@aiassesstech/prompt-shield';
|
|
135
|
+
import(promptShieldMod)
|
|
136
|
+
.then(({ PromptShield, OutputSanitizer, createSocialContentShield, createFleetShieldMiddleware, createValidatedToolHandler, nole_wallet_fund, nole_propose, nole_recruit, nole_moltbook_post, nole_x_post, nole_x_thread, nole_approve: noleApproveSchema, nole_veto: noleVetoSchema, NOLE_VETO_BUSINESS_RULES, nole_contract: noleContractSchema, NOLE_PROPOSE_BUSINESS_RULES, nole_status: noleStatusSchema, nole_assess: noleAssessSchema, nole_wallet: noleWalletSchema, nole_intel: noleIntelSchema, nole_setup: noleSetupSchema, nole_alliance_status: noleAllianceStatusSchema, nole_generate_referral: noleGenerateReferralSchema, nole_directory: noleDirectorySchema, nole_review_pending: noleReviewPendingSchema, nole_fleet_overview: noleFleetOverviewSchema, nole_moltbook_comment: noleMoltbookCommentSchema, nole_moltbook_feed: noleMoltbookFeedSchema, nole_moltbook_search: noleMoltbookSearchSchema, nole_linkedin_draft: noleLinkedinDraftSchema, nole_social_status: noleSocialStatusSchema, fleet_propose: fleetProposeSchema, }) => {
|
|
137
|
+
const shield = new PromptShield({ sensitivity: 'high' });
|
|
138
|
+
socialShield = createSocialContentShield({
|
|
139
|
+
shield,
|
|
140
|
+
platform: 'moltbook',
|
|
141
|
+
blockInjectedContent: true,
|
|
142
|
+
});
|
|
143
|
+
outputSanitizer = new OutputSanitizer({ enabled: true });
|
|
144
|
+
fleetShieldMiddleware = createFleetShieldMiddleware({ shield });
|
|
145
|
+
validatedToolFactory = createValidatedToolHandler;
|
|
146
|
+
validationSchemas = {
|
|
147
|
+
nole_wallet_fund,
|
|
148
|
+
nole_propose,
|
|
149
|
+
nole_recruit,
|
|
150
|
+
nole_moltbook_post,
|
|
151
|
+
nole_x_post,
|
|
152
|
+
nole_x_thread,
|
|
153
|
+
nole_approve: noleApproveSchema,
|
|
154
|
+
nole_veto: noleVetoSchema,
|
|
155
|
+
nole_contract: noleContractSchema,
|
|
156
|
+
nole_status: noleStatusSchema,
|
|
157
|
+
nole_assess: noleAssessSchema,
|
|
158
|
+
nole_wallet: noleWalletSchema,
|
|
159
|
+
nole_intel: noleIntelSchema,
|
|
160
|
+
nole_setup: noleSetupSchema,
|
|
161
|
+
nole_alliance_status: noleAllianceStatusSchema,
|
|
162
|
+
nole_generate_referral: noleGenerateReferralSchema,
|
|
163
|
+
nole_directory: noleDirectorySchema,
|
|
164
|
+
nole_review_pending: noleReviewPendingSchema,
|
|
165
|
+
nole_fleet_overview: noleFleetOverviewSchema,
|
|
166
|
+
nole_moltbook_comment: noleMoltbookCommentSchema,
|
|
167
|
+
nole_moltbook_feed: noleMoltbookFeedSchema,
|
|
168
|
+
nole_moltbook_search: noleMoltbookSearchSchema,
|
|
169
|
+
nole_linkedin_draft: noleLinkedinDraftSchema,
|
|
170
|
+
nole_social_status: noleSocialStatusSchema,
|
|
171
|
+
fleet_propose: fleetProposeSchema,
|
|
172
|
+
};
|
|
173
|
+
validationBusinessRules = {
|
|
174
|
+
nole_propose: NOLE_PROPOSE_BUSINESS_RULES,
|
|
175
|
+
nole_veto: NOLE_VETO_BUSINESS_RULES,
|
|
176
|
+
};
|
|
177
|
+
console.log('[nole] PromptShield social scanner + output sanitizer + behavioral validation: active (25 tools, approve/veto/contract enforce, rest monitor)');
|
|
178
|
+
})
|
|
179
|
+
.catch(() => {
|
|
180
|
+
console.log('[nole] PromptShield unavailable — social content scanner disabled');
|
|
181
|
+
});
|
|
182
|
+
if (moltbook)
|
|
183
|
+
console.log('[nole] MoltBook: configured');
|
|
184
|
+
else
|
|
185
|
+
console.log('[nole] MoltBook: not configured — set moltbookApiKey');
|
|
186
|
+
if (xClient)
|
|
187
|
+
console.log('[nole] X/Twitter: configured');
|
|
188
|
+
else
|
|
189
|
+
console.log('[nole] X/Twitter: not configured — set xApiKey + xApiSecret + xAccessToken + xAccessTokenSecret');
|
|
190
|
+
console.log('[nole] LinkedIn: draft mode (no API — Greg publishes manually)');
|
|
191
|
+
console.log(`[nole] Social posting mode: ${config.socialPostingMode}`);
|
|
192
|
+
// Callbacks stored for pending social posts (executed when Commander approves)
|
|
193
|
+
const pendingSocialCallbacks = new Map();
|
|
194
|
+
let noleAlertBus = null;
|
|
195
|
+
const fleetAlertingMod = '@aiassesstech/fleet-alerting';
|
|
196
|
+
import(fleetAlertingMod)
|
|
197
|
+
.then(({ AlertBus, AuditTrailRouter }) => {
|
|
198
|
+
noleAlertBus = new AlertBus();
|
|
199
|
+
noleAlertBus.registerRouter('audit', new AuditTrailRouter({ auditDir: join(dataDir, 'alerts', 'audit') }));
|
|
200
|
+
console.log('[nole] Fleet alerting sidecar initialized');
|
|
201
|
+
})
|
|
202
|
+
.catch(() => { });
|
|
203
|
+
const NOLE_ENFORCE_TOOLS = new Set(['nole_approve', 'nole_veto', 'nole_contract']);
|
|
204
|
+
const validationConfig = {
|
|
205
|
+
enabled: true,
|
|
206
|
+
auditFailures: true,
|
|
207
|
+
retryOnFailure: true,
|
|
208
|
+
maxRetries: 2,
|
|
209
|
+
strictMode: false,
|
|
210
|
+
mode: 'monitor',
|
|
211
|
+
};
|
|
212
|
+
function validationErrorResponse(message) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: message }],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function validateToolParams(toolName, toolCallId, params, businessRules) {
|
|
219
|
+
if (!validatedToolFactory)
|
|
220
|
+
return { ok: true, params };
|
|
221
|
+
const schema = validationSchemas[toolName];
|
|
222
|
+
if (!schema)
|
|
223
|
+
return { ok: true, params };
|
|
224
|
+
const passthrough = async (p) => p;
|
|
225
|
+
const wrapped = validatedToolFactory(passthrough, schema, String(toolName), config.agentId, { ...validationConfig, alertPublisher: noleAlertBus ?? undefined, criticalTools: NOLE_ENFORCE_TOOLS }, businessRules);
|
|
226
|
+
const result = await wrapped(params, {
|
|
227
|
+
sessionId: toolCallId,
|
|
228
|
+
sourceAgent: "unknown",
|
|
229
|
+
trustLevel: 2,
|
|
230
|
+
});
|
|
231
|
+
if (typeof result === "string") {
|
|
232
|
+
return { ok: false, message: result };
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, params: result };
|
|
235
|
+
}
|
|
236
|
+
async function governedSocialPost(payload) {
|
|
237
|
+
if (socialShield) {
|
|
238
|
+
const scan = socialShield(payload.description);
|
|
239
|
+
if (!scan.allowed) {
|
|
240
|
+
return {
|
|
241
|
+
mode: 'supervised',
|
|
242
|
+
outcome: 'rejected',
|
|
243
|
+
proposalId: `shield-block-${Date.now()}`,
|
|
244
|
+
reason: scan.reason ?? `PromptShield blocked social content (${scan.result.severity})`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (outputSanitizer) {
|
|
249
|
+
const sanitized = outputSanitizer.sanitize(payload.content, config.agentId);
|
|
250
|
+
if (sanitized.redacted || sanitized.canaryLeaked) {
|
|
251
|
+
return {
|
|
252
|
+
mode: 'supervised',
|
|
253
|
+
outcome: 'rejected',
|
|
254
|
+
proposalId: `sanitize-block-${Date.now()}`,
|
|
255
|
+
reason: sanitized.canaryLeaked
|
|
256
|
+
? 'Output sanitizer blocked canary token leakage attempt.'
|
|
257
|
+
: `Output sanitizer blocked secret leakage (${sanitized.redactionCount} redaction${sanitized.redactionCount === 1 ? '' : 's'}).`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (config.socialPostingMode === 'autonomous') {
|
|
262
|
+
return { mode: 'autonomous', result: await payload.executeFn() };
|
|
263
|
+
}
|
|
264
|
+
const result = await governance.propose({
|
|
265
|
+
actionType: 'social',
|
|
266
|
+
description: payload.description,
|
|
267
|
+
riskLevel: 'low',
|
|
268
|
+
metadata: { platform: payload.platform, action: payload.action },
|
|
269
|
+
});
|
|
270
|
+
if (result.finalOutcome === 'executed') {
|
|
271
|
+
return { mode: 'supervised', outcome: 'approved', result: await payload.executeFn() };
|
|
272
|
+
}
|
|
273
|
+
if (result.finalOutcome === 'pending') {
|
|
274
|
+
pendingSocialCallbacks.set(result.proposalId, payload.executeFn);
|
|
275
|
+
return { mode: 'supervised', outcome: 'pending_review', proposalId: result.proposalId };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
mode: 'supervised',
|
|
279
|
+
outcome: 'rejected',
|
|
280
|
+
proposalId: result.proposalId,
|
|
281
|
+
reason: result.vetoExplanation ?? 'Governance rejected',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
104
284
|
// Initialize store on startup (eager, not lazy)
|
|
105
285
|
store.initialize().then(() => {
|
|
106
286
|
console.log('[nole] Data store initialized');
|
|
@@ -152,6 +332,9 @@ export default function register(api) {
|
|
|
152
332
|
properties: {},
|
|
153
333
|
},
|
|
154
334
|
async execute(_toolCallId, _params) {
|
|
335
|
+
const sv = await validateToolParams("nole_status", _toolCallId, _params);
|
|
336
|
+
if (!sv.ok)
|
|
337
|
+
return validationErrorResponse(sv.message);
|
|
155
338
|
const stats = await governance.getStats();
|
|
156
339
|
const schedule = await scheduler.getSchedule();
|
|
157
340
|
const latestScore = await publisher.getLatest();
|
|
@@ -233,12 +416,16 @@ export default function register(api) {
|
|
|
233
416
|
},
|
|
234
417
|
async execute(_toolCallId, params) {
|
|
235
418
|
try {
|
|
419
|
+
const validated = await validateToolParams("nole_propose", _toolCallId, params, validationBusinessRules.nole_propose);
|
|
420
|
+
if (!validated.ok)
|
|
421
|
+
return validationErrorResponse(validated.message);
|
|
422
|
+
const safeParams = validated.params;
|
|
236
423
|
const result = await governance.propose({
|
|
237
|
-
actionType:
|
|
238
|
-
description:
|
|
239
|
-
estimatedCostUsd:
|
|
240
|
-
targetAgent:
|
|
241
|
-
platform:
|
|
424
|
+
actionType: safeParams.actionType, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
425
|
+
description: safeParams.description,
|
|
426
|
+
estimatedCostUsd: safeParams.estimatedCostUsd,
|
|
427
|
+
targetAgent: safeParams.targetAgent,
|
|
428
|
+
platform: safeParams.platform,
|
|
242
429
|
});
|
|
243
430
|
return {
|
|
244
431
|
content: [{
|
|
@@ -280,6 +467,9 @@ export default function register(api) {
|
|
|
280
467
|
},
|
|
281
468
|
},
|
|
282
469
|
async execute(_toolCallId, params) {
|
|
470
|
+
const av = await validateToolParams("nole_assess", _toolCallId, params);
|
|
471
|
+
if (!av.ok)
|
|
472
|
+
return validationErrorResponse(av.message);
|
|
283
473
|
const trigger = triggerEval.createOnDemand(params.reason);
|
|
284
474
|
return {
|
|
285
475
|
content: [{
|
|
@@ -303,6 +493,9 @@ export default function register(api) {
|
|
|
303
493
|
properties: {},
|
|
304
494
|
},
|
|
305
495
|
async execute(_toolCallId, _params) {
|
|
496
|
+
const wv = await validateToolParams("nole_wallet", _toolCallId, _params);
|
|
497
|
+
if (!wv.ok)
|
|
498
|
+
return validationErrorResponse(wv.message);
|
|
306
499
|
try {
|
|
307
500
|
const balance = await wallet.getBalance();
|
|
308
501
|
const runwayDays = balance.balanceUsdc > 0
|
|
@@ -363,6 +556,10 @@ export default function register(api) {
|
|
|
363
556
|
required: ["token"],
|
|
364
557
|
},
|
|
365
558
|
async execute(_toolCallId, params) {
|
|
559
|
+
const validated = await validateToolParams("nole_wallet_fund", _toolCallId, params);
|
|
560
|
+
if (!validated.ok)
|
|
561
|
+
return validationErrorResponse(validated.message);
|
|
562
|
+
const safeParams = validated.params;
|
|
366
563
|
if (wallet.network !== 'base-sepolia') {
|
|
367
564
|
return {
|
|
368
565
|
content: [{
|
|
@@ -385,7 +582,7 @@ export default function register(api) {
|
|
|
385
582
|
}
|
|
386
583
|
const results = {};
|
|
387
584
|
const coinbaseWallet = wallet;
|
|
388
|
-
const tokens =
|
|
585
|
+
const tokens = safeParams.token === 'both' ? ['eth', 'usdc'] : [safeParams.token];
|
|
389
586
|
for (const t of tokens) {
|
|
390
587
|
try {
|
|
391
588
|
const txHash = await coinbaseWallet.requestTestFunds(t);
|
|
@@ -446,6 +643,9 @@ export default function register(api) {
|
|
|
446
643
|
required: ["query"],
|
|
447
644
|
},
|
|
448
645
|
async execute(_toolCallId, params) {
|
|
646
|
+
const contractv = await validateToolParams("nole_contract", _toolCallId, params);
|
|
647
|
+
if (!contractv.ok)
|
|
648
|
+
return validationErrorResponse(contractv.message);
|
|
449
649
|
try {
|
|
450
650
|
const creds = await import('./cli/configure.js').then(m => m.loadCredentials());
|
|
451
651
|
if (!creds?.contractAddress) {
|
|
@@ -619,6 +819,9 @@ export default function register(api) {
|
|
|
619
819
|
properties: {},
|
|
620
820
|
},
|
|
621
821
|
async execute(_toolCallId, _params) {
|
|
822
|
+
const iv = await validateToolParams("nole_intel", _toolCallId, _params);
|
|
823
|
+
if (!iv.ok)
|
|
824
|
+
return validationErrorResponse(iv.message);
|
|
622
825
|
const stats = await governance.getStats();
|
|
623
826
|
return {
|
|
624
827
|
content: [{
|
|
@@ -650,6 +853,9 @@ export default function register(api) {
|
|
|
650
853
|
properties: {},
|
|
651
854
|
},
|
|
652
855
|
async execute(_toolCallId, _params) {
|
|
856
|
+
const suv = await validateToolParams("nole_setup", _toolCallId, _params);
|
|
857
|
+
if (!suv.ok)
|
|
858
|
+
return validationErrorResponse(suv.message);
|
|
653
859
|
const checks = {
|
|
654
860
|
configValid: true,
|
|
655
861
|
storeInitialized: existsSync(dataDir),
|
|
@@ -702,6 +908,10 @@ export default function register(api) {
|
|
|
702
908
|
required: ["targetWallet", "agentName", "agentPlatform"],
|
|
703
909
|
},
|
|
704
910
|
async execute(_toolCallId, params) {
|
|
911
|
+
const validated = await validateToolParams("nole_recruit", _toolCallId, params);
|
|
912
|
+
if (!validated.ok)
|
|
913
|
+
return validationErrorResponse(validated.message);
|
|
914
|
+
const safeParams = validated.params;
|
|
705
915
|
if (!platform.isConfigured) {
|
|
706
916
|
return {
|
|
707
917
|
content: [{
|
|
@@ -715,12 +925,12 @@ export default function register(api) {
|
|
|
715
925
|
};
|
|
716
926
|
}
|
|
717
927
|
const result = await platform.subscribe({
|
|
718
|
-
walletAddress:
|
|
719
|
-
tier:
|
|
720
|
-
agentName:
|
|
721
|
-
agentPlatform:
|
|
928
|
+
walletAddress: safeParams.targetWallet,
|
|
929
|
+
tier: safeParams.tier || "SCOUT",
|
|
930
|
+
agentName: safeParams.agentName,
|
|
931
|
+
agentPlatform: safeParams.agentPlatform,
|
|
722
932
|
recruiterWallet: config.platformWalletAddress,
|
|
723
|
-
referralCode:
|
|
933
|
+
referralCode: safeParams.referralCode,
|
|
724
934
|
});
|
|
725
935
|
if (!result.ok) {
|
|
726
936
|
return {
|
|
@@ -730,7 +940,7 @@ export default function register(api) {
|
|
|
730
940
|
status: "recruitment_failed",
|
|
731
941
|
error: result.error,
|
|
732
942
|
httpStatus: result.status,
|
|
733
|
-
target:
|
|
943
|
+
target: safeParams.agentName,
|
|
734
944
|
}, null, 2),
|
|
735
945
|
}],
|
|
736
946
|
};
|
|
@@ -740,10 +950,10 @@ export default function register(api) {
|
|
|
740
950
|
type: "text",
|
|
741
951
|
text: JSON.stringify({
|
|
742
952
|
status: "recruited",
|
|
743
|
-
agent:
|
|
744
|
-
wallet:
|
|
745
|
-
tier:
|
|
746
|
-
platform:
|
|
953
|
+
agent: safeParams.agentName,
|
|
954
|
+
wallet: safeParams.targetWallet,
|
|
955
|
+
tier: safeParams.tier || "SCOUT",
|
|
956
|
+
platform: safeParams.agentPlatform,
|
|
747
957
|
subscription: result.data,
|
|
748
958
|
note: "Agent has been subscribed to the AIAssessTech Trust Alliance. They will receive an API key for assessments.",
|
|
749
959
|
}, null, 2),
|
|
@@ -760,6 +970,9 @@ export default function register(api) {
|
|
|
760
970
|
properties: {},
|
|
761
971
|
},
|
|
762
972
|
async execute(_toolCallId, _params) {
|
|
973
|
+
const asv = await validateToolParams("nole_alliance_status", _toolCallId, _params);
|
|
974
|
+
if (!asv.ok)
|
|
975
|
+
return validationErrorResponse(asv.message);
|
|
763
976
|
if (!platform.isConfigured) {
|
|
764
977
|
return {
|
|
765
978
|
content: [{
|
|
@@ -803,6 +1016,9 @@ export default function register(api) {
|
|
|
803
1016
|
properties: {},
|
|
804
1017
|
},
|
|
805
1018
|
async execute(_toolCallId, _params) {
|
|
1019
|
+
const rv = await validateToolParams("nole_generate_referral", _toolCallId, _params);
|
|
1020
|
+
if (!rv.ok)
|
|
1021
|
+
return validationErrorResponse(rv.message);
|
|
806
1022
|
if (!platform.isConfigured) {
|
|
807
1023
|
return {
|
|
808
1024
|
content: [{
|
|
@@ -858,6 +1074,9 @@ export default function register(api) {
|
|
|
858
1074
|
},
|
|
859
1075
|
},
|
|
860
1076
|
async execute(_toolCallId, params) {
|
|
1077
|
+
const dv = await validateToolParams("nole_directory", _toolCallId, params);
|
|
1078
|
+
if (!dv.ok)
|
|
1079
|
+
return validationErrorResponse(dv.message);
|
|
861
1080
|
const result = await platform.getDirectory({
|
|
862
1081
|
tier: params.tier,
|
|
863
1082
|
limit: params.limit || 20,
|
|
@@ -900,6 +1119,9 @@ export default function register(api) {
|
|
|
900
1119
|
properties: {},
|
|
901
1120
|
},
|
|
902
1121
|
async execute(_toolCallId, _params) {
|
|
1122
|
+
const rpv = await validateToolParams("nole_review_pending", _toolCallId, _params);
|
|
1123
|
+
if (!rpv.ok)
|
|
1124
|
+
return validationErrorResponse(rpv.message);
|
|
903
1125
|
const pending = await governance.getPendingReviews();
|
|
904
1126
|
if (pending.length === 0) {
|
|
905
1127
|
return {
|
|
@@ -979,12 +1201,42 @@ export default function register(api) {
|
|
|
979
1201
|
},
|
|
980
1202
|
required: ["proposalId"],
|
|
981
1203
|
},
|
|
982
|
-
// BB modification #2: Access control via callingAgentId.
|
|
983
|
-
// Known limitation: OpenClaw doesn't currently pass calling agent context.
|
|
984
1204
|
async execute(_toolCallId, params) {
|
|
1205
|
+
const validated = await validateToolParams("nole_approve", _toolCallId, params);
|
|
1206
|
+
if (!validated.ok)
|
|
1207
|
+
return validationErrorResponse(validated.message);
|
|
985
1208
|
const callingAgentId = "unknown";
|
|
986
1209
|
try {
|
|
987
1210
|
const result = await governance.commanderApprove(params.proposalId, callingAgentId, params.note);
|
|
1211
|
+
// Execute pending social post callback if this was a social-post proposal
|
|
1212
|
+
let socialPostResult;
|
|
1213
|
+
const socialCallback = pendingSocialCallbacks.get(params.proposalId);
|
|
1214
|
+
if (socialCallback) {
|
|
1215
|
+
try {
|
|
1216
|
+
const callbackResult = await socialCallback();
|
|
1217
|
+
pendingSocialCallbacks.delete(params.proposalId);
|
|
1218
|
+
socialPostResult = {
|
|
1219
|
+
socialPostExecuted: true,
|
|
1220
|
+
postSuccess: callbackResult.ok,
|
|
1221
|
+
postError: callbackResult.error,
|
|
1222
|
+
};
|
|
1223
|
+
if (callbackResult.ok) {
|
|
1224
|
+
const interaction = {
|
|
1225
|
+
id: generateSocialId(),
|
|
1226
|
+
platform: result.proposal.metadata?.platform ?? 'moltbook',
|
|
1227
|
+
action: 'post',
|
|
1228
|
+
contentPreview: result.proposal.description.slice(0, 100),
|
|
1229
|
+
timestamp: new Date().toISOString(),
|
|
1230
|
+
success: true,
|
|
1231
|
+
};
|
|
1232
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
catch (cbErr) {
|
|
1236
|
+
const cbMsg = cbErr instanceof Error ? cbErr.message : String(cbErr);
|
|
1237
|
+
socialPostResult = { socialPostExecuted: false, socialPostError: cbMsg };
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
988
1240
|
return {
|
|
989
1241
|
content: [{
|
|
990
1242
|
type: "text",
|
|
@@ -995,6 +1247,7 @@ export default function register(api) {
|
|
|
995
1247
|
description: result.proposal.description,
|
|
996
1248
|
approvedBy: callingAgentId,
|
|
997
1249
|
auditHash: result.auditHash,
|
|
1250
|
+
...socialPostResult,
|
|
998
1251
|
}, null, 2),
|
|
999
1252
|
}],
|
|
1000
1253
|
};
|
|
@@ -1025,8 +1278,10 @@ export default function register(api) {
|
|
|
1025
1278
|
},
|
|
1026
1279
|
required: ["proposalId", "explanation"],
|
|
1027
1280
|
},
|
|
1028
|
-
// BB modification #6: Veto explanation quality metric
|
|
1029
1281
|
async execute(_toolCallId, params) {
|
|
1282
|
+
const vetov = await validateToolParams("nole_veto", _toolCallId, params, validationBusinessRules.nole_veto);
|
|
1283
|
+
if (!vetov.ok)
|
|
1284
|
+
return validationErrorResponse(vetov.message);
|
|
1030
1285
|
const callingAgentId = "unknown";
|
|
1031
1286
|
const quality = assessVetoQuality(params.explanation);
|
|
1032
1287
|
try {
|
|
@@ -1070,6 +1325,9 @@ export default function register(api) {
|
|
|
1070
1325
|
properties: {},
|
|
1071
1326
|
},
|
|
1072
1327
|
async execute(_toolCallId, _params) {
|
|
1328
|
+
const fov = await validateToolParams("nole_fleet_overview", _toolCallId, _params);
|
|
1329
|
+
if (!fov.ok)
|
|
1330
|
+
return validationErrorResponse(fov.message);
|
|
1073
1331
|
const stats = await governance.getStats();
|
|
1074
1332
|
const pending = await governance.getPendingReviews();
|
|
1075
1333
|
const learning = await governance.vetoTracker.isLearning();
|
|
@@ -1145,6 +1403,443 @@ export default function register(api) {
|
|
|
1145
1403
|
};
|
|
1146
1404
|
},
|
|
1147
1405
|
});
|
|
1406
|
+
// ═══════════════════════════════════════════════════════
|
|
1407
|
+
// SOCIAL MEDIA TOOLS (MoltBook, X/Twitter, LinkedIn)
|
|
1408
|
+
// All registered unconditionally per rule 155.
|
|
1409
|
+
// Client availability checked at call time.
|
|
1410
|
+
// ═══════════════════════════════════════════════════════
|
|
1411
|
+
api.registerTool({
|
|
1412
|
+
name: "nole_moltbook_post",
|
|
1413
|
+
description: "Post content to a MoltBook submolt. Nole uses this to evangelize AI governance " +
|
|
1414
|
+
"and recruit agents to the Trust Alliance. Requires moltbookApiKey in config.",
|
|
1415
|
+
parameters: {
|
|
1416
|
+
type: "object",
|
|
1417
|
+
properties: {
|
|
1418
|
+
submolt: {
|
|
1419
|
+
type: "string",
|
|
1420
|
+
description: "Target submolt (e.g., 'governance', 'aithoughts', 'ethics', 'trust')",
|
|
1421
|
+
},
|
|
1422
|
+
title: {
|
|
1423
|
+
type: "string",
|
|
1424
|
+
description: "Post title — concise, engaging, trust-focused",
|
|
1425
|
+
},
|
|
1426
|
+
content: {
|
|
1427
|
+
type: "string",
|
|
1428
|
+
description: "Post body — informative, not spammy. Include soft CTA for Grillo assessment.",
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
required: ["submolt", "title", "content"],
|
|
1432
|
+
},
|
|
1433
|
+
async execute(_toolCallId, params) {
|
|
1434
|
+
const validated = await validateToolParams("nole_moltbook_post", _toolCallId, params);
|
|
1435
|
+
if (!validated.ok)
|
|
1436
|
+
return validationErrorResponse(validated.message);
|
|
1437
|
+
const safeParams = validated.params;
|
|
1438
|
+
if (!moltbook) {
|
|
1439
|
+
return {
|
|
1440
|
+
content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured. Set moltbookApiKey in Nole plugin config.", action: "Ask Greg to register on MoltBook and provide the API key." }, null, 2) }],
|
|
1441
|
+
isError: true,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
const governed = await governedSocialPost({
|
|
1445
|
+
platform: 'moltbook',
|
|
1446
|
+
action: 'post',
|
|
1447
|
+
description: `MoltBook post to m/${safeParams.submolt}: "${safeParams.title}"`,
|
|
1448
|
+
content: `${safeParams.title}\n${safeParams.content}`,
|
|
1449
|
+
executeFn: () => moltbook.createPost(safeParams.submolt, safeParams.title, safeParams.content),
|
|
1450
|
+
});
|
|
1451
|
+
if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
|
|
1452
|
+
return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Post queued for Commander approval. Use nole_review_pending to check status." }, null, 2) }] };
|
|
1453
|
+
}
|
|
1454
|
+
if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
|
|
1455
|
+
return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
|
|
1456
|
+
}
|
|
1457
|
+
const result = governed.result;
|
|
1458
|
+
const interaction = {
|
|
1459
|
+
id: generateSocialId(),
|
|
1460
|
+
platform: 'moltbook',
|
|
1461
|
+
action: 'post',
|
|
1462
|
+
contentPreview: params.title.slice(0, 100),
|
|
1463
|
+
timestamp: new Date().toISOString(),
|
|
1464
|
+
success: result.ok,
|
|
1465
|
+
errorMessage: result.error,
|
|
1466
|
+
metadata: { submolt: params.submolt },
|
|
1467
|
+
};
|
|
1468
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1469
|
+
return {
|
|
1470
|
+
content: [{ type: "text", text: JSON.stringify(result.ok
|
|
1471
|
+
? { posted: true, postId: result.data?.id, submolt: safeParams.submolt, title: safeParams.title, mode: governed.mode }
|
|
1472
|
+
: { posted: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
|
|
1473
|
+
...(result.ok ? {} : { isError: true }),
|
|
1474
|
+
};
|
|
1475
|
+
},
|
|
1476
|
+
});
|
|
1477
|
+
api.registerTool({
|
|
1478
|
+
name: "nole_moltbook_comment",
|
|
1479
|
+
description: "Comment on a MoltBook post. Use to engage with agents discussing trust, safety, " +
|
|
1480
|
+
"governance, or behavioral assessment. Quality over quantity — build karma.",
|
|
1481
|
+
parameters: {
|
|
1482
|
+
type: "object",
|
|
1483
|
+
properties: {
|
|
1484
|
+
postId: { type: "string", description: "ID of the post to comment on" },
|
|
1485
|
+
content: {
|
|
1486
|
+
type: "string",
|
|
1487
|
+
description: "Comment text — thoughtful, helpful, trust-focused. Not spammy.",
|
|
1488
|
+
},
|
|
1489
|
+
},
|
|
1490
|
+
required: ["postId", "content"],
|
|
1491
|
+
},
|
|
1492
|
+
async execute(_toolCallId, params) {
|
|
1493
|
+
const mcv = await validateToolParams("nole_moltbook_comment", _toolCallId, params);
|
|
1494
|
+
if (!mcv.ok)
|
|
1495
|
+
return validationErrorResponse(mcv.message);
|
|
1496
|
+
if (!moltbook) {
|
|
1497
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
|
|
1498
|
+
}
|
|
1499
|
+
const governed = await governedSocialPost({
|
|
1500
|
+
platform: 'moltbook',
|
|
1501
|
+
action: 'comment',
|
|
1502
|
+
description: `MoltBook comment on post ${params.postId}: "${params.content.slice(0, 80)}..."`,
|
|
1503
|
+
content: params.content,
|
|
1504
|
+
executeFn: () => moltbook.createComment(params.postId, params.content),
|
|
1505
|
+
});
|
|
1506
|
+
if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
|
|
1507
|
+
return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Comment queued for Commander approval." }, null, 2) }] };
|
|
1508
|
+
}
|
|
1509
|
+
if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
|
|
1510
|
+
return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
|
|
1511
|
+
}
|
|
1512
|
+
const result = governed.result;
|
|
1513
|
+
const interaction = {
|
|
1514
|
+
id: generateSocialId(),
|
|
1515
|
+
platform: 'moltbook',
|
|
1516
|
+
action: 'comment',
|
|
1517
|
+
contentPreview: params.content.slice(0, 100),
|
|
1518
|
+
targetId: params.postId,
|
|
1519
|
+
timestamp: new Date().toISOString(),
|
|
1520
|
+
success: result.ok,
|
|
1521
|
+
errorMessage: result.error,
|
|
1522
|
+
};
|
|
1523
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1524
|
+
return {
|
|
1525
|
+
content: [{ type: "text", text: JSON.stringify(result.ok
|
|
1526
|
+
? { commented: true, commentId: result.data?.id, postId: params.postId, mode: governed.mode }
|
|
1527
|
+
: { commented: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
|
|
1528
|
+
...(result.ok ? {} : { isError: true }),
|
|
1529
|
+
};
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1532
|
+
api.registerTool({
|
|
1533
|
+
name: "nole_moltbook_feed",
|
|
1534
|
+
description: "Read MoltBook feed for discovery. Browse posts from specific submolts to find " +
|
|
1535
|
+
"agents discussing governance, trust, and AI safety topics.",
|
|
1536
|
+
parameters: {
|
|
1537
|
+
type: "object",
|
|
1538
|
+
properties: {
|
|
1539
|
+
submolt: {
|
|
1540
|
+
type: "string",
|
|
1541
|
+
description: "Submolt to browse (e.g., 'governance', 'aithoughts'). Omit for general feed.",
|
|
1542
|
+
},
|
|
1543
|
+
limit: {
|
|
1544
|
+
type: "number",
|
|
1545
|
+
description: "Number of posts to fetch (default 10, max 25)",
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
async execute(_toolCallId, params) {
|
|
1550
|
+
const mfv = await validateToolParams("nole_moltbook_feed", _toolCallId, params);
|
|
1551
|
+
if (!mfv.ok)
|
|
1552
|
+
return validationErrorResponse(mfv.message);
|
|
1553
|
+
if (!moltbook) {
|
|
1554
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
|
|
1555
|
+
}
|
|
1556
|
+
const limit = Math.min(params.limit ?? 10, 25);
|
|
1557
|
+
const result = await moltbook.getFeed(params.submolt, undefined, limit);
|
|
1558
|
+
const interaction = {
|
|
1559
|
+
id: generateSocialId(),
|
|
1560
|
+
platform: 'moltbook',
|
|
1561
|
+
action: 'feed',
|
|
1562
|
+
contentPreview: `Feed: ${params.submolt ?? 'general'} (${limit} posts)`,
|
|
1563
|
+
timestamp: new Date().toISOString(),
|
|
1564
|
+
success: result.ok,
|
|
1565
|
+
errorMessage: result.error,
|
|
1566
|
+
};
|
|
1567
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1568
|
+
return {
|
|
1569
|
+
content: [{ type: "text", text: JSON.stringify(result.ok ? result.data : { error: result.error }, null, 2) }],
|
|
1570
|
+
...(result.ok ? {} : { isError: true }),
|
|
1571
|
+
};
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
api.registerTool({
|
|
1575
|
+
name: "nole_moltbook_search",
|
|
1576
|
+
description: "Search MoltBook for posts about governance, trust, compliance, behavioral assessment, " +
|
|
1577
|
+
"or AI safety. Use this to discover agents who could benefit from Grillo assessment.",
|
|
1578
|
+
parameters: {
|
|
1579
|
+
type: "object",
|
|
1580
|
+
properties: {
|
|
1581
|
+
query: {
|
|
1582
|
+
type: "string",
|
|
1583
|
+
description: "Search query (e.g., 'AI governance', 'trust verification', 'behavioral assessment')",
|
|
1584
|
+
},
|
|
1585
|
+
submolt: { type: "string", description: "Limit search to a specific submolt (optional)" },
|
|
1586
|
+
limit: { type: "number", description: "Number of results (default 10, max 25)" },
|
|
1587
|
+
},
|
|
1588
|
+
required: ["query"],
|
|
1589
|
+
},
|
|
1590
|
+
async execute(_toolCallId, params) {
|
|
1591
|
+
const msv = await validateToolParams("nole_moltbook_search", _toolCallId, params);
|
|
1592
|
+
if (!msv.ok)
|
|
1593
|
+
return validationErrorResponse(msv.message);
|
|
1594
|
+
if (!moltbook) {
|
|
1595
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
|
|
1596
|
+
}
|
|
1597
|
+
const limit = Math.min(params.limit ?? 10, 25);
|
|
1598
|
+
const result = await moltbook.search(params.query, params.submolt, limit);
|
|
1599
|
+
const interaction = {
|
|
1600
|
+
id: generateSocialId(),
|
|
1601
|
+
platform: 'moltbook',
|
|
1602
|
+
action: 'search',
|
|
1603
|
+
contentPreview: `Search: "${params.query}" in ${params.submolt ?? 'all'}`,
|
|
1604
|
+
timestamp: new Date().toISOString(),
|
|
1605
|
+
success: result.ok,
|
|
1606
|
+
errorMessage: result.error,
|
|
1607
|
+
};
|
|
1608
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1609
|
+
return {
|
|
1610
|
+
content: [{ type: "text", text: JSON.stringify(result.ok ? result.data : { error: result.error }, null, 2) }],
|
|
1611
|
+
...(result.ok ? {} : { isError: true }),
|
|
1612
|
+
};
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
api.registerTool({
|
|
1616
|
+
name: "nole_x_post",
|
|
1617
|
+
description: "Post a tweet from Nole's X/Twitter account. Max 280 characters. " +
|
|
1618
|
+
"Use for AI governance thought leadership. Voice: confident, data-driven, not salesy.",
|
|
1619
|
+
parameters: {
|
|
1620
|
+
type: "object",
|
|
1621
|
+
properties: {
|
|
1622
|
+
text: {
|
|
1623
|
+
type: "string",
|
|
1624
|
+
description: "Tweet text (max 280 characters). Include hashtags: #AIGovernance #AITrust",
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
required: ["text"],
|
|
1628
|
+
},
|
|
1629
|
+
async execute(_toolCallId, params) {
|
|
1630
|
+
const validated = await validateToolParams("nole_x_post", _toolCallId, params);
|
|
1631
|
+
if (!validated.ok)
|
|
1632
|
+
return validationErrorResponse(validated.message);
|
|
1633
|
+
const safeParams = validated.params;
|
|
1634
|
+
if (!xClient) {
|
|
1635
|
+
return {
|
|
1636
|
+
content: [{ type: "text", text: JSON.stringify({ error: "X/Twitter not configured. Set xApiKey, xApiSecret, xAccessToken, xAccessTokenSecret.", action: "Ask Greg to create an X Developer App and provide the 4 credentials." }, null, 2) }],
|
|
1637
|
+
isError: true,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const governed = await governedSocialPost({
|
|
1641
|
+
platform: 'x-twitter',
|
|
1642
|
+
action: 'post',
|
|
1643
|
+
description: `X/Twitter tweet: "${safeParams.text.slice(0, 100)}${safeParams.text.length > 100 ? '...' : ''}"`,
|
|
1644
|
+
content: safeParams.text,
|
|
1645
|
+
executeFn: () => xClient.postTweet(safeParams.text),
|
|
1646
|
+
});
|
|
1647
|
+
if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
|
|
1648
|
+
return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Tweet queued for Commander approval." }, null, 2) }] };
|
|
1649
|
+
}
|
|
1650
|
+
if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
|
|
1651
|
+
return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
|
|
1652
|
+
}
|
|
1653
|
+
const result = governed.result;
|
|
1654
|
+
const interaction = {
|
|
1655
|
+
id: generateSocialId(),
|
|
1656
|
+
platform: 'x-twitter',
|
|
1657
|
+
action: 'post',
|
|
1658
|
+
contentPreview: params.text.slice(0, 100),
|
|
1659
|
+
timestamp: new Date().toISOString(),
|
|
1660
|
+
success: result.ok,
|
|
1661
|
+
errorMessage: result.error,
|
|
1662
|
+
};
|
|
1663
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1664
|
+
return {
|
|
1665
|
+
content: [{ type: "text", text: JSON.stringify(result.ok
|
|
1666
|
+
? { posted: true, tweetId: result.data?.data?.id, text: safeParams.text, mode: governed.mode }
|
|
1667
|
+
: { posted: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
|
|
1668
|
+
...(result.ok ? {} : { isError: true }),
|
|
1669
|
+
};
|
|
1670
|
+
},
|
|
1671
|
+
});
|
|
1672
|
+
api.registerTool({
|
|
1673
|
+
name: "nole_x_thread",
|
|
1674
|
+
description: "Post a thread of tweets from Nole's X/Twitter account. Each tweet max 280 chars. " +
|
|
1675
|
+
"Use for longer AI governance content that doesn't fit in a single tweet.",
|
|
1676
|
+
parameters: {
|
|
1677
|
+
type: "object",
|
|
1678
|
+
properties: {
|
|
1679
|
+
tweets: {
|
|
1680
|
+
type: "array",
|
|
1681
|
+
items: { type: "string" },
|
|
1682
|
+
description: "Array of tweet texts, posted in order as a thread. Each max 280 chars.",
|
|
1683
|
+
},
|
|
1684
|
+
},
|
|
1685
|
+
required: ["tweets"],
|
|
1686
|
+
},
|
|
1687
|
+
async execute(_toolCallId, params) {
|
|
1688
|
+
const validated = await validateToolParams("nole_x_thread", _toolCallId, params);
|
|
1689
|
+
if (!validated.ok)
|
|
1690
|
+
return validationErrorResponse(validated.message);
|
|
1691
|
+
const safeParams = validated.params;
|
|
1692
|
+
if (!xClient) {
|
|
1693
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "X/Twitter not configured." }, null, 2) }], isError: true };
|
|
1694
|
+
}
|
|
1695
|
+
const governed = await governedSocialPost({
|
|
1696
|
+
platform: 'x-twitter',
|
|
1697
|
+
action: 'thread',
|
|
1698
|
+
description: `X/Twitter thread (${safeParams.tweets.length} tweets): "${safeParams.tweets[0]?.slice(0, 60) ?? ''}..."`,
|
|
1699
|
+
content: safeParams.tweets.join('\n'),
|
|
1700
|
+
executeFn: () => xClient.postThread(safeParams.tweets),
|
|
1701
|
+
});
|
|
1702
|
+
if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
|
|
1703
|
+
return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", tweetCount: safeParams.tweets.length, note: "Thread queued for Commander approval." }, null, 2) }] };
|
|
1704
|
+
}
|
|
1705
|
+
if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
|
|
1706
|
+
return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
|
|
1707
|
+
}
|
|
1708
|
+
const result = governed.result;
|
|
1709
|
+
const interaction = {
|
|
1710
|
+
id: generateSocialId(),
|
|
1711
|
+
platform: 'x-twitter',
|
|
1712
|
+
action: 'post',
|
|
1713
|
+
contentPreview: `Thread (${params.tweets.length} tweets): ${params.tweets[0]?.slice(0, 60) ?? ''}...`,
|
|
1714
|
+
timestamp: new Date().toISOString(),
|
|
1715
|
+
success: result.ok,
|
|
1716
|
+
errorMessage: result.error,
|
|
1717
|
+
};
|
|
1718
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1719
|
+
return {
|
|
1720
|
+
content: [{ type: "text", text: JSON.stringify(result.ok
|
|
1721
|
+
? { posted: true, tweetCount: result.data?.tweets?.length, threadUrl: result.data?.threadUrl, mode: governed.mode }
|
|
1722
|
+
: { posted: false, error: result.error }, null, 2) }],
|
|
1723
|
+
...(result.ok ? {} : { isError: true }),
|
|
1724
|
+
};
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
api.registerTool({
|
|
1728
|
+
name: "nole_linkedin_draft",
|
|
1729
|
+
description: "Generate a LinkedIn post draft for Greg to review and publish manually. " +
|
|
1730
|
+
"LinkedIn has no API access — Nole drafts, Greg publishes. " +
|
|
1731
|
+
"Drafts saved to .nole-data/social/linkedin-drafts/.",
|
|
1732
|
+
parameters: {
|
|
1733
|
+
type: "object",
|
|
1734
|
+
properties: {
|
|
1735
|
+
content: {
|
|
1736
|
+
type: "string",
|
|
1737
|
+
description: "LinkedIn post text. Professional tone, B2B-focused.",
|
|
1738
|
+
},
|
|
1739
|
+
topic: {
|
|
1740
|
+
type: "string",
|
|
1741
|
+
description: "Topic category (e.g., 'AI Governance', 'Trust Architecture', 'Agent Economics')",
|
|
1742
|
+
},
|
|
1743
|
+
hashtags: {
|
|
1744
|
+
type: "array",
|
|
1745
|
+
items: { type: "string" },
|
|
1746
|
+
description: "LinkedIn hashtags (e.g., ['#AIGovernance', '#ResponsibleAI'])",
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
required: ["content", "topic"],
|
|
1750
|
+
},
|
|
1751
|
+
async execute(_toolCallId, params) {
|
|
1752
|
+
const ldv = await validateToolParams("nole_linkedin_draft", _toolCallId, params);
|
|
1753
|
+
if (!ldv.ok)
|
|
1754
|
+
return validationErrorResponse(ldv.message);
|
|
1755
|
+
const result = await linkedInDrafter.createDraft(params.content, params.topic, params.hashtags ?? []);
|
|
1756
|
+
if (result.ok && result.data) {
|
|
1757
|
+
const interaction = {
|
|
1758
|
+
id: generateSocialId(),
|
|
1759
|
+
platform: 'linkedin',
|
|
1760
|
+
action: 'draft',
|
|
1761
|
+
contentPreview: params.content.slice(0, 100),
|
|
1762
|
+
timestamp: new Date().toISOString(),
|
|
1763
|
+
success: true,
|
|
1764
|
+
metadata: { topic: params.topic },
|
|
1765
|
+
};
|
|
1766
|
+
store.saveSocialInteraction(interaction).catch(() => { });
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
content: [{ type: "text", text: JSON.stringify(result.ok
|
|
1770
|
+
? { drafted: true, draftId: result.data?.id, topic: params.topic, note: "Draft saved. Greg should review and publish manually on LinkedIn." }
|
|
1771
|
+
: { drafted: false, error: result.error }, null, 2) }],
|
|
1772
|
+
...(result.ok ? {} : { isError: true }),
|
|
1773
|
+
};
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
api.registerTool({
|
|
1777
|
+
name: "nole_social_status",
|
|
1778
|
+
description: "Show social media connectivity status: which platforms are configured, " +
|
|
1779
|
+
"rate limiter state (remaining tokens per bucket), recent interaction counts, " +
|
|
1780
|
+
"and posting mode (supervised/autonomous).",
|
|
1781
|
+
parameters: {
|
|
1782
|
+
type: "object",
|
|
1783
|
+
properties: {},
|
|
1784
|
+
},
|
|
1785
|
+
async execute(_toolCallId, _params) {
|
|
1786
|
+
const ssv = await validateToolParams("nole_social_status", _toolCallId, _params);
|
|
1787
|
+
if (!ssv.ok)
|
|
1788
|
+
return validationErrorResponse(ssv.message);
|
|
1789
|
+
const moltbookStatus = moltbook
|
|
1790
|
+
? {
|
|
1791
|
+
configured: true,
|
|
1792
|
+
rateLimits: {
|
|
1793
|
+
general: moltbookLimiter.getStatus('moltbook-general'),
|
|
1794
|
+
post: moltbookLimiter.getStatus('moltbook-post'),
|
|
1795
|
+
comment: moltbookLimiter.getStatus('moltbook-comment'),
|
|
1796
|
+
},
|
|
1797
|
+
}
|
|
1798
|
+
: { configured: false, reason: 'moltbookApiKey not set' };
|
|
1799
|
+
const xStatus = xClient
|
|
1800
|
+
? {
|
|
1801
|
+
configured: true,
|
|
1802
|
+
handle: config.xHandle,
|
|
1803
|
+
rateLimits: {
|
|
1804
|
+
general: xLimiter.getStatus('x-general'),
|
|
1805
|
+
post: xLimiter.getStatus('x-post'),
|
|
1806
|
+
},
|
|
1807
|
+
}
|
|
1808
|
+
: { configured: false, reason: 'xApiKey/xApiSecret/xAccessToken/xAccessTokenSecret not set' };
|
|
1809
|
+
const linkedInStatus = { configured: true, mode: 'draft-only (Greg publishes manually)' };
|
|
1810
|
+
let recentInteractions = {};
|
|
1811
|
+
try {
|
|
1812
|
+
const all = await store.listSocialInteractions(undefined, 200);
|
|
1813
|
+
for (const i of all) {
|
|
1814
|
+
const key = `${i.platform}:${i.action}`;
|
|
1815
|
+
recentInteractions[key] = (recentInteractions[key] ?? 0) + 1;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
catch {
|
|
1819
|
+
recentInteractions = {};
|
|
1820
|
+
}
|
|
1821
|
+
const recentFailures = await store
|
|
1822
|
+
.listSocialInteractions(undefined, 200)
|
|
1823
|
+
.then((all) => all.filter((i) => !i.success).slice(0, 10))
|
|
1824
|
+
.catch(() => []);
|
|
1825
|
+
return {
|
|
1826
|
+
content: [{
|
|
1827
|
+
type: "text",
|
|
1828
|
+
text: JSON.stringify({
|
|
1829
|
+
postingMode: config.socialPostingMode,
|
|
1830
|
+
platforms: { moltbook: moltbookStatus, x: xStatus, linkedin: linkedInStatus },
|
|
1831
|
+
interactionCounts: recentInteractions,
|
|
1832
|
+
recentFailures: recentFailures.map((f) => ({
|
|
1833
|
+
platform: f.platform,
|
|
1834
|
+
action: f.action,
|
|
1835
|
+
error: f.errorMessage,
|
|
1836
|
+
timestamp: f.timestamp,
|
|
1837
|
+
})),
|
|
1838
|
+
}, null, 2),
|
|
1839
|
+
}],
|
|
1840
|
+
};
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1148
1843
|
// --- Command (matches Grillo/NOAH pattern: sync handler, ctx.args) ---
|
|
1149
1844
|
api.registerCommand({
|
|
1150
1845
|
name: "nole",
|
|
@@ -1175,6 +1870,15 @@ export default function register(api) {
|
|
|
1175
1870
|
"- `nole_intel` — Intelligence status\n" +
|
|
1176
1871
|
"- `nole_fleet_overview` — Commander fleet briefing\n" +
|
|
1177
1872
|
"- `nole_setup` — Configuration check\n\n" +
|
|
1873
|
+
"**Social Media:**\n" +
|
|
1874
|
+
"- `nole_moltbook_post` — Post to a MoltBook submolt\n" +
|
|
1875
|
+
"- `nole_moltbook_comment` — Comment on a MoltBook post\n" +
|
|
1876
|
+
"- `nole_moltbook_feed` — Browse MoltBook feed for discovery\n" +
|
|
1877
|
+
"- `nole_moltbook_search` — Search MoltBook for governance topics\n" +
|
|
1878
|
+
"- `nole_x_post` — Post a tweet (280 char max)\n" +
|
|
1879
|
+
"- `nole_x_thread` — Post a tweet thread\n" +
|
|
1880
|
+
"- `nole_linkedin_draft` — Draft LinkedIn post for Greg to publish\n" +
|
|
1881
|
+
"- `nole_social_status` — Show social connectivity, rate limits, posting mode\n\n" +
|
|
1178
1882
|
`**Config:** Model: ${config.inferenceModel}, Commander: ${config.commanderId}`,
|
|
1179
1883
|
};
|
|
1180
1884
|
}
|
|
@@ -1197,19 +1901,69 @@ export default function register(api) {
|
|
|
1197
1901
|
};
|
|
1198
1902
|
},
|
|
1199
1903
|
});
|
|
1200
|
-
// ── Fleet
|
|
1904
|
+
// ── Fleet Tools (synchronous stubs + async hot-swap) ─────────
|
|
1905
|
+
const noleFleetToolImpls = {};
|
|
1906
|
+
api.registerTool({
|
|
1907
|
+
name: "fleet_propose",
|
|
1908
|
+
description: "Propose an action to the fleet for cross-agent governance review.",
|
|
1909
|
+
parameters: {
|
|
1910
|
+
type: "object",
|
|
1911
|
+
properties: {
|
|
1912
|
+
title: { type: "string" },
|
|
1913
|
+
description: { type: "string" },
|
|
1914
|
+
category: { type: "string", enum: ["financial", "operational", "strategic"] },
|
|
1915
|
+
estimatedImpact: { type: "string" },
|
|
1916
|
+
requiresVetoReview: { type: "boolean" },
|
|
1917
|
+
},
|
|
1918
|
+
required: ["title", "description", "category"],
|
|
1919
|
+
},
|
|
1920
|
+
async execute(_toolCallId, params) {
|
|
1921
|
+
const fv = await validateToolParams("fleet_propose", _toolCallId, params);
|
|
1922
|
+
if (!fv.ok)
|
|
1923
|
+
return validationErrorResponse(fv.message);
|
|
1924
|
+
if (noleFleetToolImpls["fleet_propose"]) {
|
|
1925
|
+
return noleFleetToolImpls["fleet_propose"].execute(_toolCallId, fv.params);
|
|
1926
|
+
}
|
|
1927
|
+
return { content: [{ type: "text", text: "Fleet bus not connected — proposal queued locally." }] };
|
|
1928
|
+
},
|
|
1929
|
+
});
|
|
1201
1930
|
const fleetBusMod = "@aiassesstech/fleet-bus";
|
|
1202
1931
|
import(fleetBusMod)
|
|
1203
1932
|
.then(async ({ FleetBus, NOLE_CARD, createNoleFleetTools, createTransport, fleetReceive }) => {
|
|
1204
|
-
const bus = FleetBus.create(api, {
|
|
1933
|
+
const bus = FleetBus.create(api, {
|
|
1934
|
+
agentId: "nole",
|
|
1935
|
+
role: "operator",
|
|
1936
|
+
card: NOLE_CARD,
|
|
1937
|
+
shield: fleetShieldMiddleware
|
|
1938
|
+
? { enabled: true, scanInbound: fleetShieldMiddleware }
|
|
1939
|
+
: undefined,
|
|
1940
|
+
});
|
|
1205
1941
|
bus.start();
|
|
1206
1942
|
const transport = await createTransport();
|
|
1207
1943
|
if (transport) {
|
|
1208
|
-
const tools = createNoleFleetTools(bus,
|
|
1944
|
+
const tools = createNoleFleetTools(bus, {
|
|
1945
|
+
...transport,
|
|
1946
|
+
outputSanitizer: outputSanitizer
|
|
1947
|
+
? {
|
|
1948
|
+
enabled: true,
|
|
1949
|
+
sanitizeOutbound: (message) => {
|
|
1950
|
+
const sanitized = outputSanitizer.sanitizeObject(message, config.agentId);
|
|
1951
|
+
return {
|
|
1952
|
+
message: sanitized.value,
|
|
1953
|
+
redacted: sanitized.redacted,
|
|
1954
|
+
redactionCount: sanitized.redactionCount,
|
|
1955
|
+
};
|
|
1956
|
+
},
|
|
1957
|
+
}
|
|
1958
|
+
: undefined,
|
|
1959
|
+
shield: fleetShieldMiddleware
|
|
1960
|
+
? { enabled: true, scanOutbound: fleetShieldMiddleware }
|
|
1961
|
+
: undefined,
|
|
1962
|
+
});
|
|
1209
1963
|
for (const tool of tools) {
|
|
1210
|
-
|
|
1964
|
+
noleFleetToolImpls[tool.name] = tool;
|
|
1211
1965
|
}
|
|
1212
|
-
console.log(`[nole] Fleet tools
|
|
1966
|
+
console.log(`[nole] Fleet tools hot-swapped: ${tools.map((t) => t.name).join(", ")}`);
|
|
1213
1967
|
fleetReceive(bus, {
|
|
1214
1968
|
methods: [
|
|
1215
1969
|
"task/assign", "proposal/approved", "proposal/rejected",
|
|
@@ -1229,7 +1983,9 @@ export default function register(api) {
|
|
|
1229
1983
|
});
|
|
1230
1984
|
console.log(`[nole] Plugin registered: nole_status, nole_propose, nole_assess, nole_wallet, nole_wallet_fund, ` +
|
|
1231
1985
|
`nole_contract, nole_intel, nole_setup, nole_recruit, nole_alliance_status, nole_generate_referral, ` +
|
|
1232
|
-
`nole_directory, nole_review_pending, nole_approve, nole_veto, nole_fleet_overview
|
|
1986
|
+
`nole_directory, nole_review_pending, nole_approve, nole_veto, nole_fleet_overview, ` +
|
|
1987
|
+
`nole_moltbook_post, nole_moltbook_comment, nole_moltbook_feed, nole_moltbook_search, ` +
|
|
1988
|
+
`nole_x_post, nole_x_thread, nole_linkedin_draft, nole_social_status tools; /nole command`);
|
|
1233
1989
|
console.log(`[nole] AIAssessTech API: ${platform.isConfigured ? 'configured' : 'not configured — set agentApiKey + platformWalletAddress'} (${config.platformApiUrl})`);
|
|
1234
1990
|
}
|
|
1235
1991
|
//# sourceMappingURL=plugin.js.map
|