@xuda.io/ai_module 1.1.5645 → 1.1.5646

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/index.mjs CHANGED
@@ -7279,10 +7279,96 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}${dashboard_s
7279
7279
  }
7280
7280
  };
7281
7281
 
7282
+ // ── Cross-tenant agent-tool guard ───────────────────────────────────────────────────────
7283
+ // An installed agent loads its plugin from the PUBLISHER's project, and that same context
7284
+ // decides where the plugin writes. So a file the user asks an installed agent to build is
7285
+ // created in the publisher's drive, in an account the user does not own, cannot browse and
7286
+ // cannot delete from. Left unguarded that is a quiet cross-tenant data write.
7287
+ //
7288
+ // Consent is recorded per (user, agent) rather than per account, because installing a
7289
+ // second agent from the same publisher is a separate decision.
7290
+ const cross_tenant_consent_id = (uid, agent_id) =>
7291
+ `agconsent_${crypto.createHash('sha256').update(`${uid}|${agent_id}`).digest('hex').slice(0, 32)}`;
7292
+
7293
+ const cross_tenant_tool_check = async function ({ uid, tool_uid, tool_app_id, own_app_id, agent_id, agent_name, plugin_id }) {
7294
+ // Same account, or the tool already runs in the caller's own project: nothing to consent
7295
+ // to, this is the ordinary case and must stay zero-friction.
7296
+ if (!tool_uid || tool_uid === uid) return { blocked: false };
7297
+ if (tool_app_id && own_app_id && tool_app_id === own_app_id) return { blocked: false };
7298
+
7299
+ const label = agent_name || agent_id || plugin_id || 'This agent';
7300
+ let owner_name = 'another Xuda account';
7301
+ try {
7302
+ const owner = await db_module.get_couch_doc_native('xuda_accounts', tool_uid);
7303
+ owner_name = owner?.account_info?.name || owner?.account_info?.email || owner_name;
7304
+ } catch (_) { /* the name is decoration; the block does not depend on it */ }
7305
+
7306
+ let granted = false;
7307
+ try {
7308
+ const got = await db_module.get_couch_doc('xuda_sessions', cross_tenant_consent_id(uid, agent_id), true);
7309
+ granted = got?.code >= 0 && got.data?.granted === true && got.data?.tool_uid === tool_uid;
7310
+ } catch (_) { granted = false; }
7311
+
7312
+ if (granted) return { blocked: false };
7313
+
7314
+ console.warn(`[ai_module] cross-tenant tool blocked: uid ${uid} -> ${label} writes into ${tool_uid} (${tool_app_id})`);
7315
+ return {
7316
+ blocked: true,
7317
+ notice: {
7318
+ kind: 'cross_tenant_tool_consent',
7319
+ agent_id,
7320
+ agent_name: label,
7321
+ plugin_id,
7322
+ owner_uid: tool_uid,
7323
+ owner_name,
7324
+ // Plain words, because this is shown to whoever is chatting, not to a developer.
7325
+ message:
7326
+ `"${label}" needs to create files using tools that run in ${owner_name}'s workspace, not yours. ` +
7327
+ `Anything it makes would be stored there, where you cannot manage or delete it. ` +
7328
+ `Approve this agent in its settings if you want it to work that way.`,
7329
+ },
7330
+ };
7331
+ };
7332
+
7333
+ // Told to the model so it explains the missing capability instead of improvising an excuse
7334
+ // or claiming it did the work. Only used on the interactive path: an unattended auto-reply
7335
+ // has nobody to ask, so there the tool is simply withheld.
7336
+ const consent_instruction = (notices) =>
7337
+ `IMPORTANT: some of your tools are unavailable in this conversation and you must not pretend otherwise.\n` +
7338
+ notices.map((n) => `- ${n.message}`).join('\n') +
7339
+ `\nIf the user asks for something that needs those tools, say plainly that you cannot do it until they approve it, and repeat the reason above. Never claim to have created or saved a file.`;
7340
+
7341
+ // Record (or withdraw) the user's decision. Scoped to the caller: a user can only ever
7342
+ // consent on their own behalf, never for someone else.
7343
+ export const set_agent_tool_consent = async (req) => {
7344
+ try {
7345
+ const agent_id = String(req.agent_id || '').trim();
7346
+ if (!agent_id) throw new Error('agent_id_required');
7347
+ const granted = req.granted === true || req.granted === 'true';
7348
+ const _id = cross_tenant_consent_id(req.uid, agent_id);
7349
+ const existing = await db_module.get_couch_doc('xuda_sessions', _id, true);
7350
+ const doc = existing?.code >= 0 ? existing.data : { _id, docType: 'agent_tool_consent', created_ts: Date.now() };
7351
+ doc.uid = req.uid;
7352
+ doc.agent_id = agent_id;
7353
+ doc.tool_uid = String(req.owner_uid || '') || doc.tool_uid || null;
7354
+ doc.granted = granted;
7355
+ doc.updated_ts = Date.now();
7356
+ await db_module.save_couch_doc('xuda_sessions', doc);
7357
+ console.log(`[ai_module] agent tool consent ${granted ? 'GRANTED' : 'withdrawn'}: uid ${req.uid} agent ${agent_id}`);
7358
+ return { code: 1, data: { agent_id, granted } };
7359
+ } catch (err) {
7360
+ return { code: -1, data: err.message };
7361
+ }
7362
+ };
7363
+
7282
7364
  const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_type, prompt_suggestion_activated, chat_suggestion_activated, gtp_token, uid, account_profile_info, job_id, headers, context, app_id }) {
7283
7365
  let tools = [];
7284
7366
  let tool_resources = {};
7285
7367
  let eligible_agent = true;
7368
+ // Tools withheld this turn because they would write into an account the caller does not
7369
+ // own. Returned so the caller can tell the user what to approve instead of the agent
7370
+ // simply appearing to have lost a capability.
7371
+ const consent_required = [];
7286
7372
 
7287
7373
  const add_xuda_public_website_tool = function ({ name, description, origin, path_prefix }) {
7288
7374
  if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
@@ -7862,9 +7948,41 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
7862
7948
 
7863
7949
  const { plugin_method, type, plugin_id, ...params } = val;
7864
7950
 
7951
+ // An INSTALLED agent runs its plugin in the PUBLISHER's project, because that is
7952
+ // where the plugin package lives. That context also decides where the tool WRITES,
7953
+ // so anything it produces (a deck, a document, a render) lands in the publisher's
7954
+ // drive rather than the drive of the person who asked for it. That is a cross-tenant
7955
+ // write: the user's content ends up in an account they do not own and cannot manage.
7956
+ //
7957
+ // Gate it on explicit, recorded consent. Refusing here rather than deeper down is
7958
+ // deliberate: the tool is never handed to the model, so the agent cannot write first
7959
+ // and ask later.
7960
+ const tool_app_id =
7961
+ ai_agent_doc?.reference_doc?.studio_meta?.shared_from_app_id || ai_agent_doc?.reference_doc?.studio_meta?.installed_from_app_id || account_profile_info.app_id;
7962
+ const tool_uid =
7963
+ ai_agent_doc?.reference_doc?.studio_meta?.shared_from_uid || ai_agent_doc?.reference_doc?.studio_meta?.installed_from_app_uid || uid;
7964
+
7965
+ const cross_tenant = await cross_tenant_tool_check({
7966
+ uid,
7967
+ tool_uid,
7968
+ tool_app_id,
7969
+ own_app_id: account_profile_info.app_id,
7970
+ agent_id: ai_agent_doc?._id || ai_agent_doc?.reference_doc?._id,
7971
+ agent_name: ai_agent_doc?.reference_doc?.properties?.menuName || ai_agent_doc?.agent_name,
7972
+ plugin_id: val.plugin_id,
7973
+ });
7974
+
7975
+ if (cross_tenant.blocked) {
7976
+ // Surfaced to the user through the agent instead of failing silently, and the
7977
+ // tool is withheld for this turn.
7978
+ eligible_agent = false;
7979
+ consent_required.push(cross_tenant.notice);
7980
+ break;
7981
+ }
7982
+
7865
7983
  const plugin_tool = await get_plugin_tool(
7866
- ai_agent_doc?.reference_doc?.studio_meta?.shared_from_app_id || ai_agent_doc?.reference_doc?.studio_meta?.installed_from_app_id || account_profile_info.app_id,
7867
- ai_agent_doc?.reference_doc?.studio_meta?.shared_from_uid || ai_agent_doc?.reference_doc?.studio_meta?.installed_from_app_uid || uid,
7984
+ tool_app_id,
7985
+ tool_uid,
7868
7986
  val.plugin_id,
7869
7987
  val.plugin_method,
7870
7988
  params,
@@ -7882,7 +8000,7 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
7882
8000
  }
7883
8001
  }
7884
8002
 
7885
- return { tools, tool_resources, eligible_agent, context };
8003
+ return { tools, tool_resources, eligible_agent, context, consent_required };
7886
8004
  };
7887
8005
 
7888
8006
  const load_ai_agent_doc = async function (app_id, agent_id) {
@@ -8266,7 +8384,11 @@ const ai_chat_conversation = async function (req, job_id, headers) {
8266
8384
  const agent_name = `${ai_agent_doc._id}`;
8267
8385
  const agent = new Agent({
8268
8386
  name: agent_name.substring(0, 55),
8269
- instructions: ai_agent_doc.agentConfig.agent_instructions + (reference_type === 'ai_agents' ? get_agent_instructions() : '') + (context?.has_full_stack_vps ? '\n\n' + get_full_stack_vps_instructions() : ''),
8387
+ instructions:
8388
+ ai_agent_doc.agentConfig.agent_instructions +
8389
+ (reference_type === 'ai_agents' ? get_agent_instructions() : '') +
8390
+ (context?.has_full_stack_vps ? '\n\n' + get_full_stack_vps_instructions() : '') +
8391
+ (tools_ret.consent_required?.length ? '\n\n' + consent_instruction(tools_ret.consent_required) : ''),
8270
8392
  model: resolve_ai_model(model),
8271
8393
  tools,
8272
8394
  metadata: {
package/index_ms.mjs CHANGED
@@ -25,6 +25,10 @@ export const classify_external_app_scan = async function (...args) {
25
25
  return await broker.send_to_queue("classify_external_app_scan", ...args);
26
26
  };
27
27
 
28
+ export const set_agent_tool_consent = async function (...args) {
29
+ return await broker.send_to_queue("set_agent_tool_consent", ...args);
30
+ };
31
+
28
32
  export const execute_codex_request = async function (...args) {
29
33
  return await broker.send_to_queue("execute_codex_request", ...args);
30
34
  };
package/index_msa.mjs CHANGED
@@ -25,6 +25,10 @@ export const classify_external_app_scan = function (...args) {
25
25
  broker.send_to_queue_async("classify_external_app_scan", ...args);
26
26
  };
27
27
 
28
+ export const set_agent_tool_consent = function (...args) {
29
+ broker.send_to_queue_async("set_agent_tool_consent", ...args);
30
+ };
31
+
28
32
  export const execute_codex_request = function (...args) {
29
33
  broker.send_to_queue_async("execute_codex_request", ...args);
30
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5645",
3
+ "version": "1.1.5646",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",