@xuda.io/ai_module 1.1.5645 → 1.1.5647
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 +210 -4
- package/index_ms.mjs +8 -0
- package/index_msa.mjs +8 -0
- package/package.json +1 -1
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
|
-
|
|
7867
|
-
|
|
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:
|
|
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: {
|
|
@@ -11163,6 +11285,90 @@ async function normalizeBase64To1024(
|
|
|
11163
11285
|
return normalizedBuffer.toString('base64');
|
|
11164
11286
|
}
|
|
11165
11287
|
|
|
11288
|
+
// Upload-time content check for a profile picture or a business logo.
|
|
11289
|
+
//
|
|
11290
|
+
// Deliberately NOT identity verification. It answers "is this the right KIND of
|
|
11291
|
+
// image, and is it good enough to use", never "is this the right person". There
|
|
11292
|
+
// is no face embedding, no matching against a reference, and nothing about the
|
|
11293
|
+
// image is retained — so this does not cross the biometric line drawn in
|
|
11294
|
+
// docs/plans/xuda-verify-consent.md, which stays gated on the face-model
|
|
11295
|
+
// decision and that document's pre-capture prerequisites. Identity matching is
|
|
11296
|
+
// still the unbuilt half of phase 2, see
|
|
11297
|
+
// docs/plans/handoff_profile_image_verification.md.
|
|
11298
|
+
//
|
|
11299
|
+
// The same classification already existed as a private closure inside
|
|
11300
|
+
// get_profile_avatar (inspect_person_in_image), but it only ran at AVATAR
|
|
11301
|
+
// GENERATION time, which is after verification. Users need the answer while
|
|
11302
|
+
// they are still looking at the picker, so it lives here as its own method and
|
|
11303
|
+
// covers the business/logo case the original never did.
|
|
11304
|
+
export const inspect_profile_picture = async function (req, job_id, headers) {
|
|
11305
|
+
try {
|
|
11306
|
+
const data = req?.data || req || {};
|
|
11307
|
+
const uid = req?.uid;
|
|
11308
|
+
const url = data.url;
|
|
11309
|
+
const account_type = data.account_type === 'business' ? 'business' : 'personal';
|
|
11310
|
+
|
|
11311
|
+
if (!uid) return { code: -401, data: 'not authorized' };
|
|
11312
|
+
if (!url) return { code: -1, data: 'url is required' };
|
|
11313
|
+
|
|
11314
|
+
// Attribution only; a missing profile must not block the check.
|
|
11315
|
+
let account_profile_info;
|
|
11316
|
+
try {
|
|
11317
|
+
account_profile_info = await account_ms.get_active_account_profile_info(uid);
|
|
11318
|
+
} catch (e) {}
|
|
11319
|
+
|
|
11320
|
+
const image_blob_ret = await get_image_blob_from_downloaded_image(url);
|
|
11321
|
+
if (!image_blob_ret?.image_blob) return { code: -1, data: 'could not read that image' };
|
|
11322
|
+
const base64 = Buffer.from(await image_blob_ret.image_blob.arrayBuffer()).toString('base64');
|
|
11323
|
+
|
|
11324
|
+
const is_business = account_type === 'business';
|
|
11325
|
+
const instruction = is_business
|
|
11326
|
+
? `This image was uploaded as the COMPANY LOGO for a business account. Decide whether it is usable as that logo or brand mark. Reject a photograph of a person, a selfie, a stock photo, a screenshot, a meme, an unrelated object, or an image with no identifiable mark. Also reject it if it is too blurry, too small or too cropped to use.`
|
|
11327
|
+
: `This image was uploaded as a PERSONAL PROFILE PICTURE. Decide whether it is a genuine photograph of a real person, suitable as an authentic account portrait. Reject a company logo, an illustration, a cartoon, an obviously AI-generated render, an object, an animal, a screenshot, or a group shot with no single clear subject. Also reject it if the face is not visible, heavily cropped, very blurry, or too small.`;
|
|
11328
|
+
|
|
11329
|
+
const ret = await submit_chat_gpt_prompt({
|
|
11330
|
+
uid,
|
|
11331
|
+
model: _conf.default_ai_model,
|
|
11332
|
+
prompt: [
|
|
11333
|
+
{
|
|
11334
|
+
role: 'user',
|
|
11335
|
+
content: [
|
|
11336
|
+
{ type: 'input_text', text: `${instruction}\n\nBe practical rather than strict: an ordinary phone photo in ordinary lighting is fine. Only reject when a real person would agree it is the wrong kind of image or genuinely unusable.` },
|
|
11337
|
+
{ type: 'input_image', image_url: `data:image/png;base64,${base64}` },
|
|
11338
|
+
],
|
|
11339
|
+
},
|
|
11340
|
+
],
|
|
11341
|
+
response_format: z.object({
|
|
11342
|
+
acceptable: z.boolean().describe('true only if the image is the right kind AND usable quality'),
|
|
11343
|
+
reason_code: z
|
|
11344
|
+
.string()
|
|
11345
|
+
.describe(
|
|
11346
|
+
'snake_case code. Use ok when acceptable. Otherwise one of: not_a_person, is_a_logo, illustration, ai_generated, object_or_animal, screenshot, multiple_faces, face_not_visible, too_blurry, too_small, too_cropped, not_a_logo, no_clear_mark',
|
|
11347
|
+
),
|
|
11348
|
+
message: z.string().describe('one short, friendly sentence telling the user what to upload instead. No jargon, no mention of models or scores.'),
|
|
11349
|
+
}),
|
|
11350
|
+
metadata: { func: 'inspect_profile_picture', account_type },
|
|
11351
|
+
account_profile_info,
|
|
11352
|
+
});
|
|
11353
|
+
|
|
11354
|
+
if (ret.code < 0) return ret;
|
|
11355
|
+
|
|
11356
|
+
const res = JSON5.parse(ret.data);
|
|
11357
|
+
return {
|
|
11358
|
+
code: 1,
|
|
11359
|
+
data: {
|
|
11360
|
+
acceptable: Boolean(res?.acceptable),
|
|
11361
|
+
reason_code: res?.reason_code || (res?.acceptable ? 'ok' : 'unusable'),
|
|
11362
|
+
message: res?.message || '',
|
|
11363
|
+
account_type,
|
|
11364
|
+
},
|
|
11365
|
+
};
|
|
11366
|
+
} catch (err) {
|
|
11367
|
+
console.error('[inspect_profile_picture]', err.message);
|
|
11368
|
+
return { code: -1, data: err.message };
|
|
11369
|
+
}
|
|
11370
|
+
};
|
|
11371
|
+
|
|
11166
11372
|
const get_image_blob_from_downloaded_image = async function (url) {
|
|
11167
11373
|
async function hasTransparentBackground(filePath) {
|
|
11168
11374
|
const image = sharp(filePath);
|
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
|
};
|
|
@@ -237,6 +241,10 @@ export const is_business_contact_has_person = async function (...args) {
|
|
|
237
241
|
return await broker.send_to_queue("is_business_contact_has_person", ...args);
|
|
238
242
|
};
|
|
239
243
|
|
|
244
|
+
export const inspect_profile_picture = async function (...args) {
|
|
245
|
+
return await broker.send_to_queue("inspect_profile_picture", ...args);
|
|
246
|
+
};
|
|
247
|
+
|
|
240
248
|
export const get_name_from_email_addr = async function (...args) {
|
|
241
249
|
return await broker.send_to_queue("get_name_from_email_addr", ...args);
|
|
242
250
|
};
|
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
|
};
|
|
@@ -237,6 +241,10 @@ export const is_business_contact_has_person = function (...args) {
|
|
|
237
241
|
broker.send_to_queue_async("is_business_contact_has_person", ...args);
|
|
238
242
|
};
|
|
239
243
|
|
|
244
|
+
export const inspect_profile_picture = function (...args) {
|
|
245
|
+
broker.send_to_queue_async("inspect_profile_picture", ...args);
|
|
246
|
+
};
|
|
247
|
+
|
|
240
248
|
export const get_name_from_email_addr = function (...args) {
|
|
241
249
|
broker.send_to_queue_async("get_name_from_email_addr", ...args);
|
|
242
250
|
};
|