@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.
Files changed (39) hide show
  1. package/agent/AGENTS.md +34 -0
  2. package/dist/governance/types.d.ts.map +1 -1
  3. package/dist/governance/types.js +2 -0
  4. package/dist/governance/types.js.map +1 -1
  5. package/dist/plugin.d.ts.map +1 -1
  6. package/dist/plugin.js +781 -25
  7. package/dist/plugin.js.map +1 -1
  8. package/dist/social/linkedin-drafter.d.ts +16 -0
  9. package/dist/social/linkedin-drafter.d.ts.map +1 -0
  10. package/dist/social/linkedin-drafter.js +66 -0
  11. package/dist/social/linkedin-drafter.js.map +1 -0
  12. package/dist/social/moltbook-client.d.ts +28 -0
  13. package/dist/social/moltbook-client.d.ts.map +1 -0
  14. package/dist/social/moltbook-client.js +97 -0
  15. package/dist/social/moltbook-client.js.map +1 -0
  16. package/dist/social/rate-limiter.d.ts +22 -0
  17. package/dist/social/rate-limiter.d.ts.map +1 -0
  18. package/dist/social/rate-limiter.js +67 -0
  19. package/dist/social/rate-limiter.js.map +1 -0
  20. package/dist/social/types.d.ts +99 -0
  21. package/dist/social/types.d.ts.map +1 -0
  22. package/dist/social/types.js +6 -0
  23. package/dist/social/types.js.map +1 -0
  24. package/dist/social/x-client.d.ts +33 -0
  25. package/dist/social/x-client.d.ts.map +1 -0
  26. package/dist/social/x-client.js +167 -0
  27. package/dist/social/x-client.js.map +1 -0
  28. package/dist/store/json-store.d.ts +3 -0
  29. package/dist/store/json-store.d.ts.map +1 -1
  30. package/dist/store/json-store.js +24 -0
  31. package/dist/store/json-store.js.map +1 -1
  32. package/dist/store/types.d.ts +3 -0
  33. package/dist/store/types.d.ts.map +1 -1
  34. package/dist/types/nole-config.d.ts +24 -0
  35. package/dist/types/nole-config.d.ts.map +1 -1
  36. package/dist/types/nole-config.js +9 -0
  37. package/dist/types/nole-config.js.map +1 -1
  38. package/openclaw.plugin.json +36 -0
  39. 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: params.actionType, // eslint-disable-line @typescript-eslint/no-explicit-any
238
- description: params.description,
239
- estimatedCostUsd: params.estimatedCostUsd,
240
- targetAgent: params.targetAgent,
241
- platform: params.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 = params.token === 'both' ? ['eth', 'usdc'] : [params.token];
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: params.targetWallet,
719
- tier: params.tier || "SCOUT",
720
- agentName: params.agentName,
721
- agentPlatform: params.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: params.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: params.agentName,
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: params.agentName,
744
- wallet: params.targetWallet,
745
- tier: params.tier || "SCOUT",
746
- platform: params.agentPlatform,
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 Bus Integration (Phase 2 full onboarding) ────────
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, { agentId: "nole", role: "operator", card: NOLE_CARD });
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, transport);
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
- api.registerTool(tool);
1964
+ noleFleetToolImpls[tool.name] = tool;
1211
1965
  }
1212
- console.log(`[nole] Fleet tools registered: ${tools.map((t) => t.name).join(", ")}`);
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 tools; /nole command`);
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