@jini-ai/agentic 0.6.19 → 0.6.23
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/CHANGELOG.md +4 -0
- package/dist/core/dom/dom-page-driver.js +119 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicationFilter, publishedText } from './publication-filter.js';
|
|
1
|
+
import { createPublicationFilter, publicElement, publishedText } from './publication-filter.js';
|
|
2
2
|
import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_PRIVATE_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, AGENT_ELEMENT_ROLES, resolveHandleSelector, } from '../element-handles.js';
|
|
3
3
|
import { normalizeAgentLabel } from '../guards.js';
|
|
4
4
|
/**
|
|
@@ -6,7 +6,8 @@ import { normalizeAgentLabel } from '../guards.js';
|
|
|
6
6
|
*
|
|
7
7
|
* Deliberately mechanical: every refusal — handle validation, the credential-field guard, the
|
|
8
8
|
* page allowlist, highlight clamping — already ran in `executePageCapability` before anything
|
|
9
|
-
* here is called.
|
|
9
|
+
* here is called. Live DOM visibility and interaction state are rechecked here immediately
|
|
10
|
+
* before effects, because the page can change after capability validation.
|
|
10
11
|
*
|
|
11
12
|
* Scoped to a `root` the host names, never `document`. Scanning the whole document would make
|
|
12
13
|
* any markup anywhere — including rendered user content — an authorization decision, which
|
|
@@ -244,20 +245,22 @@ function isPrivate(node) {
|
|
|
244
245
|
return node.closest(PRIVATE_SELECTOR) !== null;
|
|
245
246
|
}
|
|
246
247
|
/**
|
|
247
|
-
* `element`'s text content with
|
|
248
|
+
* `element`'s text content with private and inactive subtrees left out — the only text
|
|
248
249
|
* this driver ever reports, so a secret shown inside a published card never rides out on the
|
|
249
|
-
* card's own text.
|
|
250
|
+
* card's own text.
|
|
251
|
+
* @returns Public text without mutating the DOM.
|
|
252
|
+
* @complexity O(n × d) time and O(n) space for subtree size n and ancestor depth d.
|
|
253
|
+
* @overallScore 95/100
|
|
254
|
+
* @qualityFindings Low: Live CSS visibility checks repeat along ancestor paths during a scan.
|
|
250
255
|
*/
|
|
251
256
|
function publicTextOf(element) {
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
/* c8 ignore next -- as above. */
|
|
260
|
-
return copy.textContent ?? '';
|
|
257
|
+
if (!publicElement({ element }))
|
|
258
|
+
return '';
|
|
259
|
+
return Array.from(element.childNodes).map(node => {
|
|
260
|
+
if (node instanceof Element)
|
|
261
|
+
return publicTextOf(node);
|
|
262
|
+
return node instanceof Text ? node.data : '';
|
|
263
|
+
}).join('');
|
|
261
264
|
}
|
|
262
265
|
function describe(element, page, publicText) {
|
|
263
266
|
const role = element.getAttribute(AGENT_ROLE_ATTRIBUTE);
|
|
@@ -306,6 +309,74 @@ function findSelectableOption({ control, handle, option }, _optional = {}) {
|
|
|
306
309
|
}
|
|
307
310
|
return match;
|
|
308
311
|
}
|
|
312
|
+
/** Resolve only label-associated fields, never arbitrary descendant buttons or links.
|
|
313
|
+
* A descendant that is itself independently published (its own `[data-agent-element]`) must
|
|
314
|
+
* never be silently adopted as this handle's control — it is a distinct, separately
|
|
315
|
+
* addressable target with its own label, not unhandled wrapper markup like the plain
|
|
316
|
+
* `<li><label><input>` this fallback exists for. Filtering to descendants whose nearest
|
|
317
|
+
* published ancestor is still `element` itself excludes exactly that case.
|
|
318
|
+
* @returns Distinct owned controls, retaining ambiguity for the caller to refuse.
|
|
319
|
+
* @complexity O(l × d + c) time and O(c) space for labels l, depth d and labelled controls c.
|
|
320
|
+
* @overallScore 96/100
|
|
321
|
+
* @qualityFindings Low: Native label relationships are read from the live DOM, so callers must recheck before effects.
|
|
322
|
+
*/
|
|
323
|
+
function labelledControls({ element, root }) {
|
|
324
|
+
const labels = Array.from(element.querySelectorAll('label'));
|
|
325
|
+
if (element instanceof HTMLLabelElement)
|
|
326
|
+
labels.unshift(element);
|
|
327
|
+
const controls = labels.flatMap(label => [label.control, ...label.querySelectorAll('input,textarea,select')]);
|
|
328
|
+
return [...new Set(controls)].filter((control) => {
|
|
329
|
+
if (!control?.matches('input,textarea,select') || isPrivate(control))
|
|
330
|
+
return false;
|
|
331
|
+
if (!(root instanceof Node) || !root.contains(control))
|
|
332
|
+
return false;
|
|
333
|
+
const owner = control.closest(`[${AGENT_ELEMENT_ATTRIBUTE}]`);
|
|
334
|
+
return owner === element || (element instanceof HTMLLabelElement && owner === null);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** Enforce live visibility, disabled and modal state before any control effect.
|
|
338
|
+
* @throws Error with the reason the current control cannot be operated.
|
|
339
|
+
* @complexity O(m × d) time and O(m) space for m modal candidates and ancestor depth d.
|
|
340
|
+
* @overallScore 95/100
|
|
341
|
+
* @qualityFindings Low: Legacy DOMs without the modal selector expose only ARIA modal state.
|
|
342
|
+
*/
|
|
343
|
+
function assertActiveControl({ element, handle, action }) {
|
|
344
|
+
if (!publicElement({ element }))
|
|
345
|
+
throw new Error(`"${handle}" is hidden or inactive on this page`);
|
|
346
|
+
// A disabled control is not one a user could touch, so writing to it is a way around the
|
|
347
|
+
// page's own rule and the resulting selection cannot be submitted. The per-`<option>` filter
|
|
348
|
+
// in `findSelectableOption` reads as if it enforced that, but only ever looked at each option — a
|
|
349
|
+
// `<select disabled>` (and a `<select>` inside a `<fieldset disabled>`, which `:disabled`
|
|
350
|
+
// also covers and `.disabled` does not) was written to happily, and the executor then
|
|
351
|
+
// reported `targetChanged: true` because the value really had changed.
|
|
352
|
+
//
|
|
353
|
+
// Checked before the option lookup on purpose: the refusal is about the control, so it must
|
|
354
|
+
// not depend on the caller having named a real option, and the "Available: ..." message must
|
|
355
|
+
// not enumerate a disabled control's options.
|
|
356
|
+
if (element.matches(':disabled') || element.closest('[aria-disabled="true"]')) {
|
|
357
|
+
if (action === 'selectOption')
|
|
358
|
+
throw new Error(`"${handle}" is disabled, so its selection cannot be changed — a user cannot change it either, and its value would not be submitted`);
|
|
359
|
+
throw new Error(`"${handle}" is disabled, so it cannot be changed`);
|
|
360
|
+
}
|
|
361
|
+
assertModalControl({ element, handle });
|
|
362
|
+
}
|
|
363
|
+
/** Respect the focused modal, including native top-layer dialogs in supporting browsers.
|
|
364
|
+
* @throws Error when the target is in the inactive background.
|
|
365
|
+
* @complexity O(m × d) time and O(m) space for m modal candidates and ancestor depth d.
|
|
366
|
+
* @overallScore 94/100
|
|
367
|
+
* @qualityFindings Low: Unfocused ARIA modal stacks use document order, matching the host's overlay convention.
|
|
368
|
+
*/
|
|
369
|
+
function assertModalControl({ element, handle }) {
|
|
370
|
+
const document = element.ownerDocument;
|
|
371
|
+
const ariaModal = '[role="dialog"][aria-modal="true"],[role="alertdialog"][aria-modal="true"]';
|
|
372
|
+
const nativeModal = document.defaultView?.CSS?.supports?.('selector(:modal)') === true;
|
|
373
|
+
const selector = nativeModal ? `:modal,${ariaModal}` : ariaModal;
|
|
374
|
+
const modals = Array.from(document.querySelectorAll(selector)).filter(modal => publicElement({ element: modal }));
|
|
375
|
+
const focused = modals.findLast(modal => modal.contains(document.activeElement));
|
|
376
|
+
const active = focused ?? modals.at(-1);
|
|
377
|
+
if (active && !active.contains(element))
|
|
378
|
+
throw new Error(`"${handle}" is behind the active modal`);
|
|
379
|
+
}
|
|
309
380
|
/**
|
|
310
381
|
* Builds a driver over `root`.
|
|
311
382
|
*
|
|
@@ -339,12 +410,15 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
339
410
|
* Every element published under `handle`. A page that (accidentally, or via a templating bug)
|
|
340
411
|
* publishes the same handle twice makes `find`'s single-match resolution ambiguous — surfaced
|
|
341
412
|
* here so both callers below can refuse rather than silently pick one.
|
|
413
|
+
* @complexity O(h × d) time and O(h) space for matching handles h and ancestor depth d.
|
|
414
|
+
* @overallScore 97/100
|
|
415
|
+
* @qualityFindings Low: Visibility checks use live computed styles for every candidate.
|
|
342
416
|
*/
|
|
343
417
|
const findAll = (handle) => {
|
|
344
418
|
const matches = Array.from(root.querySelectorAll(resolveHandleSelector({ handle })));
|
|
345
419
|
if (options.publication && matches.length !== 1)
|
|
346
420
|
return [];
|
|
347
|
-
return matches.filter(element =>
|
|
421
|
+
return matches.filter(element => publicElement({ element }) && (!publicationFilter || publicationFilter.allows({ element, action: 'read' })));
|
|
348
422
|
};
|
|
349
423
|
/**
|
|
350
424
|
* More than one element answers to the same handle — a page-authoring bug (duplicate
|
|
@@ -357,6 +431,12 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
357
431
|
const ambiguousHandleError = (handle, count) => new Error(`"${handle}" is published on ${count} elements on this page, so which one it addresses is `
|
|
358
432
|
+ 'ambiguous — this is a page-authoring bug (duplicate data-agent-element), not something a '
|
|
359
433
|
+ 'caller can resolve; publish a unique handle per element');
|
|
434
|
+
/** Resolve one handle and recheck its live action target before returning.
|
|
435
|
+
* @throws Error for missing, ambiguous or currently unavailable controls.
|
|
436
|
+
* @complexity O(h × d + l + m × d) time and O(h + l + m) space for matches h, labels l, modals m and depth d.
|
|
437
|
+
* @overallScore 96/100
|
|
438
|
+
* @qualityFindings Low: The established driver API reports refusals through Error messages.
|
|
439
|
+
*/
|
|
360
440
|
const find = (handle, action = 'read') => {
|
|
361
441
|
const matches = findAll(handle);
|
|
362
442
|
if (matches.length === 0)
|
|
@@ -364,7 +444,10 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
364
444
|
if (matches.length > 1)
|
|
365
445
|
throw ambiguousHandleError(handle, matches.length);
|
|
366
446
|
const element = matches[0];
|
|
367
|
-
|
|
447
|
+
const control = controlOf(element, action);
|
|
448
|
+
if (['click', 'fill', 'selectOption'].includes(action))
|
|
449
|
+
assertActiveControl({ element: control, handle, action });
|
|
450
|
+
if (publicationFilter && !publicationFilter.allows({ element: control, action }))
|
|
368
451
|
throw new Error('Published action unavailable');
|
|
369
452
|
return element;
|
|
370
453
|
};
|
|
@@ -374,8 +457,13 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
374
457
|
* `<details>` is called out because its interactive part is the `<summary>`: clicking the
|
|
375
458
|
* `<details>` element itself dispatches an event the platform ignores, so a disclosure widget
|
|
376
459
|
* would never open and the caller would be told the click landed.
|
|
460
|
+
* @returns The direct control or one unambiguous label-associated field; regions remain regions.
|
|
461
|
+
* @throws Error for ambiguous or out-of-root label forwarding before effects.
|
|
462
|
+
* @complexity O(l × d + c) time and O(c) space for labels l, controls c and ancestor depth d.
|
|
463
|
+
* @overallScore 96/100
|
|
464
|
+
* @qualityFindings Low: Malformed native details without a summary retain their established region-click behavior.
|
|
377
465
|
*/
|
|
378
|
-
const controlOf = (element) => {
|
|
466
|
+
const controlOf = (element, action = 'read') => {
|
|
379
467
|
// An editable region IS the control. Descending would find a link inside a rich-text document
|
|
380
468
|
// and address that instead of the text the caller meant to write.
|
|
381
469
|
if (isEditableRegion(element))
|
|
@@ -384,29 +472,29 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
384
472
|
// unscoped query returns the first summary anywhere in the subtree, so a `<details>`
|
|
385
473
|
// containing another one would toggle the inner disclosure while reporting that the outer
|
|
386
474
|
// handle was clicked — the false success this whole suite exists to catch.
|
|
387
|
-
if (element instanceof HTMLDetailsElement)
|
|
475
|
+
if (element instanceof HTMLDetailsElement)
|
|
388
476
|
return element.querySelector(':scope > summary') ?? element;
|
|
389
|
-
|
|
390
|
-
if (element.matches('input, textarea, select, button, a'))
|
|
477
|
+
if (element.matches('input, textarea, select, button, a, summary'))
|
|
391
478
|
return element;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
479
|
+
const candidates = labelledControls({ element, root })
|
|
480
|
+
.filter(control => action !== 'read' || publicElement({ element: control }));
|
|
481
|
+
if (candidates.length === 1)
|
|
482
|
+
return candidates[0];
|
|
483
|
+
if (action === 'read')
|
|
484
|
+
return element;
|
|
485
|
+
if (candidates.length > 1)
|
|
486
|
+
throw new Error('The labelled control is ambiguous; use its own handle');
|
|
487
|
+
// Clicking a label has a native forwarding effect, including to controls outside the root.
|
|
488
|
+
if (element instanceof HTMLLabelElement)
|
|
489
|
+
throw new Error('The labelled control is unavailable in this page');
|
|
490
|
+
return element;
|
|
403
491
|
};
|
|
404
492
|
const publicationFilter = options.publication ? createPublicationFilter({ root, publication: options.publication,
|
|
405
493
|
fieldOf: element => fieldDescriptorOf(controlOf(element)) ?? dropdownFieldDescriptorOf({ dropdown: element instanceof HTMLSelectElement ? element : undefined }) }) : undefined;
|
|
406
494
|
return {
|
|
407
495
|
async findElements(_required, filter = {}) {
|
|
408
496
|
const found = Array.from(root.querySelectorAll(`[${AGENT_ELEMENT_ATTRIBUTE}]`))
|
|
409
|
-
.filter(element =>
|
|
497
|
+
.filter(element => publicElement({ element }) && (!publicationFilter || findAll(element.getAttribute(AGENT_ELEMENT_ATTRIBUTE) ?? '').includes(element)))
|
|
410
498
|
.map((element) => describe(element, pageOf(element), publicationFilter ? publishedText({ element }) : undefined))
|
|
411
499
|
.filter((element) => element.handle.length > 0);
|
|
412
500
|
const query = filter.query?.toLowerCase();
|
|
@@ -562,20 +650,6 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
562
650
|
if (!(control instanceof HTMLSelectElement)) {
|
|
563
651
|
throw new Error(`"${handle}" is not a dropdown`);
|
|
564
652
|
}
|
|
565
|
-
// A disabled control is not one a user could touch, so writing to it is a way around the
|
|
566
|
-
// page's own rule and the resulting selection cannot be submitted. The per-`<option>` filter
|
|
567
|
-
// in `findSelectableOption` reads as if it enforced that, but only ever looked at each option — a
|
|
568
|
-
// `<select disabled>` (and a `<select>` inside a `<fieldset disabled>`, which `:disabled`
|
|
569
|
-
// also covers and `.disabled` does not) was written to happily, and the executor then
|
|
570
|
-
// reported `targetChanged: true` because the value really had changed.
|
|
571
|
-
//
|
|
572
|
-
// Checked before the option lookup on purpose: the refusal is about the control, so it must
|
|
573
|
-
// not depend on the caller having named a real option, and the "Available: ..." message must
|
|
574
|
-
// not enumerate a disabled control's options.
|
|
575
|
-
if (control.matches(':disabled')) {
|
|
576
|
-
throw new Error(`"${handle}" is disabled, so its selection cannot be changed — `
|
|
577
|
-
+ 'a user cannot change it either, and its value would not be submitted');
|
|
578
|
-
}
|
|
579
653
|
const match = findSelectableOption({ control, handle, option });
|
|
580
654
|
if (control.multiple) {
|
|
581
655
|
// A multi-select accumulates by default. Assigning `value` would silently clear every
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/agentic",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
4
4
|
"description": "Framework-free agent-control layer: page-capability vocabulary and manifests, the data-agent-* markup convention, AG-UI projection, generative-UI streams, and A2UI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|