@xuda.io/ai_module 1.1.5644 → 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 +177 -10
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -345,6 +345,7 @@ const LIST_SORT_PATHS = {
|
|
|
345
345
|
};
|
|
346
346
|
|
|
347
347
|
const account_ms = await import(`${module_path}/account_module/index_ms.mjs`);
|
|
348
|
+
const ticket_ms = await import(`${module_path}/ticket_module/index_ms.mjs`);
|
|
348
349
|
const drive_ms = await import(`${module_path}/drive_module/index_ms.mjs`);
|
|
349
350
|
const jobs_ms = await import(`${module_path}/jobs_module/index_ms.mjs`);
|
|
350
351
|
const team_ms = await import(`${module_path}/team_module/index_ms.mjs`);
|
|
@@ -4711,7 +4712,7 @@ export const classify_external_app_scan = async (req) => {
|
|
|
4711
4712
|
}
|
|
4712
4713
|
};
|
|
4713
4714
|
|
|
4714
|
-
// Triage a CPI error incident for the central error resolver (
|
|
4715
|
+
// Triage a CPI error incident for the central error resolver (ticket_module).
|
|
4715
4716
|
// The Zod schema lives here because zodTextFormat schemas cannot cross the
|
|
4716
4717
|
// message broker (it JSON-serializes args and strips functions). The resolver
|
|
4717
4718
|
// passes a plain-JSON { incident, catalog_entry } payload and gets back a plain
|
|
@@ -5002,10 +5003,17 @@ const get_error_message = function (err, fallback = 'Unknown error') {
|
|
|
5002
5003
|
};
|
|
5003
5004
|
|
|
5004
5005
|
const get_plugin_import_specifier = function (app_id, plugin_name, resource, dev = true) {
|
|
5005
|
-
|
|
5006
|
+
// `dev` asks for the repo working copy under $XUDA_HOME/plugins, which only
|
|
5007
|
+
// exists on the debug box. Anywhere else (master, the regions) that directory
|
|
5008
|
+
// is absent, so the plugin has to be the npm copy installed into the app's own
|
|
5009
|
+
// plugins folder. Without the is_debug gate every plugin-backed agent tool
|
|
5010
|
+
// resolved to a path that does not exist in production.
|
|
5011
|
+
const use_source = dev && _conf.is_debug;
|
|
5012
|
+
const absolute_path = use_source ? path.join(process.env.XUDA_HOME, 'plugins', plugin_name, resource) : path.join(_conf.plugins_drive_path, app_id, 'node_modules', plugin_name, resource);
|
|
5006
5013
|
|
|
5007
5014
|
const file_url = pathToFileURL(absolute_path);
|
|
5008
|
-
if (
|
|
5015
|
+
if (use_source) {
|
|
5016
|
+
// Cache-bust so an edit to the working copy is picked up without a restart.
|
|
5009
5017
|
file_url.searchParams.set('ts', `${Date.now()}`);
|
|
5010
5018
|
}
|
|
5011
5019
|
|
|
@@ -7271,10 +7279,96 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}${dashboard_s
|
|
|
7271
7279
|
}
|
|
7272
7280
|
};
|
|
7273
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
|
+
|
|
7274
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 }) {
|
|
7275
7365
|
let tools = [];
|
|
7276
7366
|
let tool_resources = {};
|
|
7277
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 = [];
|
|
7278
7372
|
|
|
7279
7373
|
const add_xuda_public_website_tool = function ({ name, description, origin, path_prefix }) {
|
|
7280
7374
|
if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
|
|
@@ -7854,9 +7948,41 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
|
|
|
7854
7948
|
|
|
7855
7949
|
const { plugin_method, type, plugin_id, ...params } = val;
|
|
7856
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
|
+
|
|
7857
7983
|
const plugin_tool = await get_plugin_tool(
|
|
7858
|
-
|
|
7859
|
-
|
|
7984
|
+
tool_app_id,
|
|
7985
|
+
tool_uid,
|
|
7860
7986
|
val.plugin_id,
|
|
7861
7987
|
val.plugin_method,
|
|
7862
7988
|
params,
|
|
@@ -7874,7 +8000,7 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
|
|
|
7874
8000
|
}
|
|
7875
8001
|
}
|
|
7876
8002
|
|
|
7877
|
-
return { tools, tool_resources, eligible_agent, context };
|
|
8003
|
+
return { tools, tool_resources, eligible_agent, context, consent_required };
|
|
7878
8004
|
};
|
|
7879
8005
|
|
|
7880
8006
|
const load_ai_agent_doc = async function (app_id, agent_id) {
|
|
@@ -8258,7 +8384,11 @@ const ai_chat_conversation = async function (req, job_id, headers) {
|
|
|
8258
8384
|
const agent_name = `${ai_agent_doc._id}`;
|
|
8259
8385
|
const agent = new Agent({
|
|
8260
8386
|
name: agent_name.substring(0, 55),
|
|
8261
|
-
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) : ''),
|
|
8262
8392
|
model: resolve_ai_model(model),
|
|
8263
8393
|
tools,
|
|
8264
8394
|
metadata: {
|
|
@@ -9112,7 +9242,9 @@ const run_plugin = async function (app_id, uid, plugin_name, method, prop_data,
|
|
|
9112
9242
|
const fields = await get_fields_data(methods[method].fields, prop_data);
|
|
9113
9243
|
const params = fields;
|
|
9114
9244
|
const env = { couch, app_id, uid, userName, agent_info };
|
|
9115
|
-
|
|
9245
|
+
// Platform credentials come from the box's secrets at call time, never from
|
|
9246
|
+
// the plugin package or a stored per-app copy. See with_platform_plugin_secrets.
|
|
9247
|
+
const setup_doc = _common.with_platform_plugin_secrets(plugin_doc.setup);
|
|
9116
9248
|
|
|
9117
9249
|
///////////////////////
|
|
9118
9250
|
const execute_server_script = async function (req, app_id_reference, server_script, method, params, setup_doc) {
|
|
@@ -9237,7 +9369,9 @@ const get_plugin_tool = async function (app_id, uid, plugin_name, method, prop_d
|
|
|
9237
9369
|
const fields = await get_fields_data(_method.fields, prop_data);
|
|
9238
9370
|
const params = fields;
|
|
9239
9371
|
const env = { couch, app_id, uid, userName, agent_info, account_profile_obj };
|
|
9240
|
-
|
|
9372
|
+
// Platform credentials come from the box's secrets at call time, never from
|
|
9373
|
+
// the plugin package or a stored per-app copy. See with_platform_plugin_secrets.
|
|
9374
|
+
const setup_doc = _common.with_platform_plugin_secrets(plugin_doc.setup);
|
|
9241
9375
|
|
|
9242
9376
|
///////////////////////
|
|
9243
9377
|
const execute_server_script = async function (req, app_id_reference, server_script, method, params, setup_doc) {
|
|
@@ -17407,7 +17541,40 @@ export const contact_form_submit = async function (req, job_id, headers) {
|
|
|
17407
17541
|
const body_parts = [message];
|
|
17408
17542
|
const detail_bits = [phone && `Phone: ${phone}`, company && `Company: ${company}`].filter(Boolean);
|
|
17409
17543
|
if (detail_bits.length) body_parts.push(detail_bits.join(' | '));
|
|
17410
|
-
await _contact_form_create_ticket(account_profile_info, contact_id, subject, body_parts.join('\n\n'), origin);
|
|
17544
|
+
const _conv = await _contact_form_create_ticket(account_profile_info, contact_id, subject, body_parts.join('\n\n'), origin);
|
|
17545
|
+
|
|
17546
|
+
// Source 6 of docs/plans/xuda-tickets.md. The conversation above stays the
|
|
17547
|
+
// human-readable thread and the contact mirror; this ALSO files the uniform
|
|
17548
|
+
// ticket so a contact-form submission shows up in the Tickets manager next
|
|
17549
|
+
// to every other source, with the same status, topic, action and SLA. Best
|
|
17550
|
+
// effort on purpose: the visitor already has their conversation, so a
|
|
17551
|
+
// ticketing failure must never fail the submission.
|
|
17552
|
+
try {
|
|
17553
|
+
const _tk = await ticket_ms.ticket_submit({
|
|
17554
|
+
source: 'profile_contact_form',
|
|
17555
|
+
channel: 'form',
|
|
17556
|
+
owner_uid: account_profile_info.uid,
|
|
17557
|
+
owner_profile_id: account_profile_info.account_profile_id,
|
|
17558
|
+
name,
|
|
17559
|
+
email,
|
|
17560
|
+
phone,
|
|
17561
|
+
company,
|
|
17562
|
+
subject,
|
|
17563
|
+
details: body_parts.join('\n\n'),
|
|
17564
|
+
origin: String(origin || '').slice(0, 200),
|
|
17565
|
+
});
|
|
17566
|
+
// Link both ways so the manager can jump to the thread and vice versa.
|
|
17567
|
+
if (_tk?.code > 0 && _tk.data?._id && _conv?._id) {
|
|
17568
|
+
const _c = await db_module.get_app_couch_doc(account_profile_info.app_id, _conv._id);
|
|
17569
|
+
if (_c.code > -1 && _c.data) {
|
|
17570
|
+
_c.data.ticket_id = _tk.data._id;
|
|
17571
|
+
_c.data.ticket_no = _tk.data.ticket_no;
|
|
17572
|
+
await db_module.save_app_couch_doc(account_profile_info.app_id, _c.data);
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
} catch (tk_err) {
|
|
17576
|
+
console.warn('[contact_form] uniform ticket not filed:', tk_err.message);
|
|
17577
|
+
}
|
|
17411
17578
|
|
|
17412
17579
|
// Owner notification email (platform template).
|
|
17413
17580
|
if (config.notify_email !== false) {
|
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
|
};
|