@jini-ai/agentic 0.4.2 → 0.4.3
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 +6 -0
- package/dist/a2ui/agent-to-renderer.d.ts +613 -0
- package/dist/a2ui/agent-to-renderer.js +191 -0
- package/dist/a2ui/catalog.d.ts +73 -0
- package/dist/a2ui/catalog.js +304 -0
- package/dist/a2ui/common-types.d.ts +401 -0
- package/dist/a2ui/common-types.js +130 -0
- package/dist/a2ui/index.d.ts +16 -0
- package/dist/a2ui/index.js +16 -0
- package/dist/a2ui/interpreter.d.ts +71 -0
- package/dist/a2ui/interpreter.js +337 -0
- package/dist/a2ui/json-pointer.d.ts +48 -0
- package/dist/a2ui/json-pointer.js +174 -0
- package/dist/a2ui/renderer-to-agent.d.ts +342 -0
- package/dist/a2ui/renderer-to-agent.js +111 -0
- package/dist/a2ui/resolve.d.ts +28 -0
- package/dist/a2ui/resolve.js +144 -0
- package/dist/a2ui/tree.d.ts +66 -0
- package/dist/a2ui/tree.js +74 -0
- package/dist/ag-ui.d.ts +82 -0
- package/dist/ag-ui.js +85 -0
- package/dist/agui/encoder.d.ts +50 -0
- package/dist/agui/encoder.js +97 -0
- package/dist/agui/events.d.ts +59 -0
- package/dist/agui/events.js +15 -0
- package/dist/capability.d.ts +81 -0
- package/dist/capability.js +79 -0
- package/dist/core/a2ui/index.d.ts +3 -7
- package/dist/core/a2ui/index.js +3 -7
- package/dist/core/a2ui/interpreter.d.ts +1 -1
- package/dist/core/a2ui/interpreter.js +0 -1
- package/dist/core/a2ui/surface-properties.d.ts +1 -1
- package/dist/core/a2ui/surface-properties.js +1 -1
- package/dist/core/dom/index.d.ts +3 -8
- package/dist/core/dom/index.js +3 -8
- package/dist/core/gen-ui/index.d.ts +2 -4
- package/dist/core/gen-ui/index.js +2 -4
- package/dist/core/index.d.ts +4 -30
- package/dist/core/index.js +4 -30
- package/dist/dom/dom-page-driver.d.ts +36 -0
- package/dist/dom/dom-page-driver.js +601 -0
- package/dist/dom/index.d.ts +17 -0
- package/dist/dom/index.js +17 -0
- package/dist/dom/model-context.d.ts +69 -0
- package/dist/dom/model-context.js +44 -0
- package/dist/element-handles.d.ts +120 -0
- package/dist/element-handles.js +39 -0
- package/dist/gen-ui/encoder.d.ts +50 -0
- package/dist/gen-ui/encoder.js +97 -0
- package/dist/gen-ui/events.d.ts +59 -0
- package/dist/gen-ui/events.js +15 -0
- package/dist/gen-ui/index.d.ts +18 -0
- package/dist/gen-ui/index.js +17 -0
- package/dist/guards.d.ts +96 -0
- package/dist/guards.js +186 -0
- package/dist/handle.d.ts +41 -0
- package/dist/handle.js +41 -0
- package/dist/index.d.ts +3 -35
- package/dist/index.js +3 -35
- package/dist/mcp-ui-apps.d.ts +223 -0
- package/dist/mcp-ui-apps.js +252 -0
- package/dist/mcp-ui.d.ts +133 -0
- package/dist/mcp-ui.js +134 -0
- package/dist/page-capabilities.d.ts +14 -0
- package/dist/page-capabilities.js +129 -0
- package/dist/page-driver.d.ts +120 -0
- package/dist/page-driver.js +2 -0
- package/dist/page-executor.d.ts +94 -0
- package/dist/page-executor.js +320 -0
- package/dist/webmcp.d.ts +202 -0
- package/dist/webmcp.js +173 -0
- package/package.json +20 -6
package/dist/guards.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module guards
|
|
3
|
+
*
|
|
4
|
+
* The refusals that must hold on every surface, expressed over plain descriptors rather than
|
|
5
|
+
* DOM nodes so they are testable in Node and reusable from any framework binding.
|
|
6
|
+
*
|
|
7
|
+
* Both guards here answer review findings that an allowlist alone does not cover: a generic
|
|
8
|
+
* filler will happily type into a password or card-number field even when that field carries a
|
|
9
|
+
* legitimate handle, and `data-agent-label` is page-authored text that reaches a model as if
|
|
10
|
+
* the host had written it.
|
|
11
|
+
*/
|
|
12
|
+
/** Input types no automated caller may ever write to. */
|
|
13
|
+
const DENIED_TYPES = new Set(['password', 'hidden', 'file', 'image']);
|
|
14
|
+
/** Autocomplete tokens covering credentials, payment instruments and one-time codes. */
|
|
15
|
+
const DENIED_AUTOCOMPLETE = new Set([
|
|
16
|
+
'current-password', 'new-password', 'one-time-code',
|
|
17
|
+
'cc-number', 'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-name', 'cc-type',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Substrings in `name`/`id` that indicate a secret even when type/autocomplete are unset.
|
|
21
|
+
*
|
|
22
|
+
* Matched against {@link squashSeparators}'d text, so the multi-word entries catch the separator
|
|
23
|
+
* spellings people actually use. `api_key`, `api-key` and `apiKey` are all the same field; a
|
|
24
|
+
* literal-substring match sees only the last one, which is the least common of the three.
|
|
25
|
+
*/
|
|
26
|
+
const SUSPICIOUS_NAME = /(?:password|passwd|secret|token|csrf|xsrf|otp|cvv|cvc|ssn|creditcard|cardnumber|apikey|ccnumber|cardnum|accountnumber|routing|iban|sortcode|securitycode|passphrase|privatekey|clientsecret|accesskey|secretkey|recoverycode|backupcode|mfa)/i;
|
|
27
|
+
// Deliberately NOT added as bare substrings, even though both are real PCI/banking terms
|
|
28
|
+
// ("pin", "PAN" — Primary Account Number): they collide with ordinary field names once
|
|
29
|
+
// separators are squashed out. `shipping` contains "pin"; `company` contains "pan". Adding
|
|
30
|
+
// either here would refuse a large share of legitimate, non-sensitive fields. See
|
|
31
|
+
// guards.test.ts's "does not refuse ordinary fields that happen to contain pin/pan as a
|
|
32
|
+
// substring" test — it exists specifically so a future edit cannot add either back silently.
|
|
33
|
+
/**
|
|
34
|
+
* Drops everything that is not a letter or digit, so separator conventions do not decide whether
|
|
35
|
+
* a guard fires. Over-matching is the safe direction here and is the point: this guard fails
|
|
36
|
+
* closed, and the cost of a false positive is a field an agent must leave to the user.
|
|
37
|
+
*
|
|
38
|
+
* Normalizes before squashing rather than after, because the naive squash used to *delete* any
|
|
39
|
+
* character outside ASCII `[a-z0-9]` instead of folding it. Deleting, not folding, is the bug:
|
|
40
|
+
* replacing one letter of a trigger word with a look-alike codepoint (fullwidth, accented, …)
|
|
41
|
+
* used to remove that letter from the squashed string entirely, so `password` (fullwidth
|
|
42
|
+
* U+FF50 `p`) squashed to `assword` and matched nothing. NFKC folds that class of look-alike —
|
|
43
|
+
* fullwidth/halfwidth forms, and the Latin compatibility block — to its ordinary ASCII form
|
|
44
|
+
* before the letter can be deleted. The NFD pass afterward strips combining marks so an accented
|
|
45
|
+
* letter (`á` → `a` + combining acute) folds the same way once the mark itself is dropped.
|
|
46
|
+
*
|
|
47
|
+
* This does NOT close the general homoglyph class. NFKC has no cross-script confusables table:
|
|
48
|
+
* Cyrillic `а` (U+0430) and Greek `ο` (U+03BF) stay distinct codepoints from Latin `a`/`o` after
|
|
49
|
+
* NFKC, so `pаssword` (Cyrillic а) still squashes to something other than `password` and still
|
|
50
|
+
* bypasses `SUSPICIOUS_NAME`. Closing that would need an explicit confusables-folding table
|
|
51
|
+
* (e.g. Unicode TR39's) mapping each look-alike script's letters onto Latin before squashing —
|
|
52
|
+
* deliberately not built here; see the "known limit" test in guards.test.ts for the concrete gap.
|
|
53
|
+
*/
|
|
54
|
+
function squashSeparators(value) {
|
|
55
|
+
const foldedWidth = value.normalize('NFKC');
|
|
56
|
+
const withoutMarks = foldedWidth.normalize('NFD').replace(/\p{M}/gu, '');
|
|
57
|
+
return withoutMarks.replace(/[^a-z0-9]/gi, '');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Input types that hold no text.
|
|
61
|
+
*
|
|
62
|
+
* Writing to one succeeds mechanically and accomplishes nothing: setting `value` on a checkbox
|
|
63
|
+
* changes the string submitted *if* it is ticked, and leaves `checked` alone. The caller is told
|
|
64
|
+
* the fill worked and the box is still clear — a false success, which is worse than a refusal
|
|
65
|
+
* because it is never retried.
|
|
66
|
+
*/
|
|
67
|
+
const NON_TEXT_TYPES = new Set(['checkbox', 'radio', 'submit', 'reset', 'button']);
|
|
68
|
+
/**
|
|
69
|
+
* Why this field's current value must not be reported back to a caller, or `null` when reading
|
|
70
|
+
* it is safe.
|
|
71
|
+
*
|
|
72
|
+
* Reading is the strictly more dangerous direction: writing into a password box gives an agent
|
|
73
|
+
* nothing, whereas reading one out hands over the secret itself. So every secrecy signal the fill
|
|
74
|
+
* guard uses applies here identically — type, autocomplete token, and a name or id that indicates
|
|
75
|
+
* a credential or anti-forgery token — and none of them may be relaxed for reads.
|
|
76
|
+
*
|
|
77
|
+
* @param field - The field's attributes.
|
|
78
|
+
* @returns The first secrecy refusal, or `null` when the value may be reported.
|
|
79
|
+
*/
|
|
80
|
+
export function findFieldReadRefusal(field) {
|
|
81
|
+
const type = field.type?.toLowerCase();
|
|
82
|
+
if (type !== undefined && DENIED_TYPES.has(type))
|
|
83
|
+
return 'denied-type';
|
|
84
|
+
const autocomplete = field.autocomplete?.toLowerCase();
|
|
85
|
+
if (autocomplete !== undefined) {
|
|
86
|
+
// The real grammar is space-separated tokens, but a comma-joined value
|
|
87
|
+
// (`"new-password,current-password"`) is markup an attacker-controlled page can trivially
|
|
88
|
+
// emit even though real browsers won't autofill it correctly either — splitting on
|
|
89
|
+
// whitespace alone turned the whole string into one unrecognized token, so the field still
|
|
90
|
+
// worked as a plain text input while looking unguarded.
|
|
91
|
+
for (const token of autocomplete.split(/[\s,]+/)) {
|
|
92
|
+
if (DENIED_AUTOCOMPLETE.has(token))
|
|
93
|
+
return 'denied-autocomplete';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (SUSPICIOUS_NAME.test(squashSeparators(field.name ?? ''))
|
|
97
|
+
|| SUSPICIOUS_NAME.test(squashSeparators(field.id ?? ''))
|
|
98
|
+
// `name`/`id` are the *machine* names; a form builder or CMS that emits `name="field_47"`
|
|
99
|
+
// next to a visible "Card number" label left the guard nothing to judge until this existed.
|
|
100
|
+
//
|
|
101
|
+
// Each label is squashed and matched on its own rather than as one joined string: squashing
|
|
102
|
+
// deletes the separators, so a join would let two innocuous labels form a trigger word across
|
|
103
|
+
// their boundary ("Address" + "Snapshot" -> "addresssnapshot", which contains "ssn"). This
|
|
104
|
+
// guard is deliberately over-inclusive, but not at the cost of matching text no page wrote.
|
|
105
|
+
|| (field.accessibleLabels ?? []).some((label) => SUSPICIOUS_NAME.test(squashSeparators(label)))) {
|
|
106
|
+
return 'suspicious-name';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Why an automated fill must be refused for this field, or `null` when it is safe to write.
|
|
112
|
+
*
|
|
113
|
+
* Fails closed on anything that looks like a credential, payment instrument, one-time code, or
|
|
114
|
+
* anti-forgery token, independent of whether the field carries an agent handle. Tagging a
|
|
115
|
+
* password box does not make it fillable. Every secrecy refusal is also a fill refusal — a field
|
|
116
|
+
* whose contents an agent may not read is certainly not one it may overwrite — and two further
|
|
117
|
+
* refusals cover fields that are simply not writable.
|
|
118
|
+
*
|
|
119
|
+
* @param field - The field's attributes.
|
|
120
|
+
* @returns The first refusal reason, or `null` when the field may be filled.
|
|
121
|
+
*/
|
|
122
|
+
export function findFieldFillRefusal(field) {
|
|
123
|
+
const secrecy = findFieldReadRefusal(field);
|
|
124
|
+
if (secrecy !== null)
|
|
125
|
+
return secrecy;
|
|
126
|
+
const type = field.type?.toLowerCase();
|
|
127
|
+
if (type !== undefined && NON_TEXT_TYPES.has(type))
|
|
128
|
+
return 'not-text';
|
|
129
|
+
if (field.readOnly === true)
|
|
130
|
+
return 'read-only';
|
|
131
|
+
if (field.disabled === true)
|
|
132
|
+
return 'disabled';
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const FIELD_REFUSAL_MESSAGES = {
|
|
136
|
+
'denied-type': 'this field type can never be filled by an agent',
|
|
137
|
+
'denied-autocomplete': 'this field holds a credential or payment instrument',
|
|
138
|
+
'suspicious-name': 'this field name indicates a secret or anti-forgery token',
|
|
139
|
+
'not-text': 'this control holds no text — activate it with the click verb instead of filling it',
|
|
140
|
+
'read-only': 'this field is read-only',
|
|
141
|
+
disabled: 'this field is disabled',
|
|
142
|
+
};
|
|
143
|
+
const FIELD_READ_REFUSAL_MESSAGES = {
|
|
144
|
+
'denied-type': 'this field type is never readable by an agent',
|
|
145
|
+
'denied-autocomplete': 'this field holds a credential or payment instrument',
|
|
146
|
+
'suspicious-name': 'this field name indicates a secret or anti-forgery token',
|
|
147
|
+
};
|
|
148
|
+
/** Model-readable reason for a refusal, so a blocked fill explains itself instead of failing opaquely. */
|
|
149
|
+
export function describeFieldRefusal(refusal) {
|
|
150
|
+
return FIELD_REFUSAL_MESSAGES[refusal];
|
|
151
|
+
}
|
|
152
|
+
/** Model-readable reason a value was withheld, so a caller knows the field has contents it may not see rather than assuming it is empty. */
|
|
153
|
+
export function describeFieldReadRefusal(refusal) {
|
|
154
|
+
return FIELD_READ_REFUSAL_MESSAGES[refusal];
|
|
155
|
+
}
|
|
156
|
+
/** Default cap on page-authored text handed to a model. */
|
|
157
|
+
export const MAX_AGENT_LABEL_LENGTH = 200;
|
|
158
|
+
/**
|
|
159
|
+
* C0/C1 control characters plus the Unicode bidirectional and invisible formatting ranges.
|
|
160
|
+
*
|
|
161
|
+
* The bidi overrides matter as much as the control codes: `U+202E` and friends let page text
|
|
162
|
+
* render one way and read another, which is a direct way to smuggle a different instruction
|
|
163
|
+
* into whatever reads the label back.
|
|
164
|
+
*/
|
|
165
|
+
const CONTROL_AND_BIDI =
|
|
166
|
+
// eslint-disable-next-line no-control-regex
|
|
167
|
+
/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
168
|
+
/**
|
|
169
|
+
* Normalizes page-authored text before it reaches a model.
|
|
170
|
+
*
|
|
171
|
+
* Strips control characters (including the bidirectional overrides used to make text render
|
|
172
|
+
* differently from how it reads), collapses whitespace, and caps the length. The result is still
|
|
173
|
+
* untrusted *content* — this only bounds it; it does not make it safe to follow as instruction.
|
|
174
|
+
*
|
|
175
|
+
* @param raw - Text read off the page, e.g. a `data-agent-label`.
|
|
176
|
+
* @param maxLength - Cap, defaulting to {@link MAX_AGENT_LABEL_LENGTH}.
|
|
177
|
+
* @returns The bounded text and whether anything was cut.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeAgentLabel(raw, maxLength = MAX_AGENT_LABEL_LENGTH) {
|
|
180
|
+
const stripped = raw.replace(CONTROL_AND_BIDI, ' ');
|
|
181
|
+
const collapsed = stripped.replace(/\s+/g, ' ').trim();
|
|
182
|
+
if (collapsed.length <= maxLength)
|
|
183
|
+
return { text: collapsed, truncated: false };
|
|
184
|
+
return { text: collapsed.slice(0, maxLength), truncated: true };
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=guards.js.map
|
package/dist/handle.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module handle
|
|
3
|
+
*
|
|
4
|
+
* `agentHandle('save')` — the attribute props a component spreads onto its root element to
|
|
5
|
+
* publish itself under this package's `data-agent-*` markup convention (see
|
|
6
|
+
* `element-handles.ts`), without the caller having to know the attribute names or re-derive the
|
|
7
|
+
* handle-validity rule itself.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately pure data, no DOM: this returns a plain object of string attributes, spreadable by
|
|
10
|
+
* React (`<button {...agentHandle('save')}>`), Vue, Svelte, or any renderer that accepts arbitrary
|
|
11
|
+
* attribute props on an element. That is what lets it live in this package's universal root
|
|
12
|
+
* rather than `./dom` — nothing here touches `document` or any browser global.
|
|
13
|
+
*/
|
|
14
|
+
import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, type AgentElementRole } from './element-handles.js';
|
|
15
|
+
/** Optional markup to attach alongside the handle itself. */
|
|
16
|
+
export interface AgentHandleOptions {
|
|
17
|
+
/** What verb applies to this element — see {@link AgentElementRole}. */
|
|
18
|
+
readonly role?: AgentElementRole;
|
|
19
|
+
/** Stable, page-authored ontology for this element (e.g. `"Full name"`). Never live text. */
|
|
20
|
+
readonly label?: string;
|
|
21
|
+
/** Which published page this element belongs to, for a multi-page host. */
|
|
22
|
+
readonly page?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The attribute props {@link agentHandle} returns, ready to spread onto an element. */
|
|
25
|
+
export type AgentHandleProps = {
|
|
26
|
+
readonly [AGENT_ELEMENT_ATTRIBUTE]: string;
|
|
27
|
+
} & Partial<Record<typeof AGENT_ROLE_ATTRIBUTE | typeof AGENT_LABEL_ATTRIBUTE | typeof AGENT_PAGE_ATTRIBUTE, string>>;
|
|
28
|
+
/**
|
|
29
|
+
* Builds the attribute props that publish `handle` as an agent-addressable element.
|
|
30
|
+
*
|
|
31
|
+
* @param handle - The handle to publish. Must satisfy {@link isValidElementHandle} — this
|
|
32
|
+
* function reuses that exact rule rather than a second, possibly-diverging check, since an
|
|
33
|
+
* adversarial probe already confirmed it rejects quotes, brackets, backslashes, unicode and
|
|
34
|
+
* overlong handles (see `element-handles.ts`).
|
|
35
|
+
* @param options - Optional role/label/page markup.
|
|
36
|
+
* @returns Attribute props to spread onto the element that should carry this handle.
|
|
37
|
+
* @throws If `handle` is not a valid element handle — never publishes a handle a caller (`page.*`
|
|
38
|
+
* capabilities, `resolveHandleSelector`) could not later resolve.
|
|
39
|
+
*/
|
|
40
|
+
export declare function agentHandle(handle: string, options?: AgentHandleOptions): AgentHandleProps;
|
|
41
|
+
//# sourceMappingURL=handle.d.ts.map
|
package/dist/handle.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module handle
|
|
3
|
+
*
|
|
4
|
+
* `agentHandle('save')` — the attribute props a component spreads onto its root element to
|
|
5
|
+
* publish itself under this package's `data-agent-*` markup convention (see
|
|
6
|
+
* `element-handles.ts`), without the caller having to know the attribute names or re-derive the
|
|
7
|
+
* handle-validity rule itself.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately pure data, no DOM: this returns a plain object of string attributes, spreadable by
|
|
10
|
+
* React (`<button {...agentHandle('save')}>`), Vue, Svelte, or any renderer that accepts arbitrary
|
|
11
|
+
* attribute props on an element. That is what lets it live in this package's universal root
|
|
12
|
+
* rather than `./dom` — nothing here touches `document` or any browser global.
|
|
13
|
+
*/
|
|
14
|
+
import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, isValidElementHandle, } from './element-handles.js';
|
|
15
|
+
/**
|
|
16
|
+
* Builds the attribute props that publish `handle` as an agent-addressable element.
|
|
17
|
+
*
|
|
18
|
+
* @param handle - The handle to publish. Must satisfy {@link isValidElementHandle} — this
|
|
19
|
+
* function reuses that exact rule rather than a second, possibly-diverging check, since an
|
|
20
|
+
* adversarial probe already confirmed it rejects quotes, brackets, backslashes, unicode and
|
|
21
|
+
* overlong handles (see `element-handles.ts`).
|
|
22
|
+
* @param options - Optional role/label/page markup.
|
|
23
|
+
* @returns Attribute props to spread onto the element that should carry this handle.
|
|
24
|
+
* @throws If `handle` is not a valid element handle — never publishes a handle a caller (`page.*`
|
|
25
|
+
* capabilities, `resolveHandleSelector`) could not later resolve.
|
|
26
|
+
*/
|
|
27
|
+
export function agentHandle(handle, options = {}) {
|
|
28
|
+
if (!isValidElementHandle(handle)) {
|
|
29
|
+
throw new Error(`invalid element handle "${handle.slice(0, 128)}": `
|
|
30
|
+
+ 'handles are lowercase words joined by single hyphens, and are never CSS selectors');
|
|
31
|
+
}
|
|
32
|
+
const props = { [AGENT_ELEMENT_ATTRIBUTE]: handle };
|
|
33
|
+
if (options.role !== undefined)
|
|
34
|
+
props[AGENT_ROLE_ATTRIBUTE] = options.role;
|
|
35
|
+
if (options.label !== undefined)
|
|
36
|
+
props[AGENT_LABEL_ATTRIBUTE] = options.label;
|
|
37
|
+
if (options.page !== undefined)
|
|
38
|
+
props[AGENT_PAGE_ATTRIBUTE] = options.page;
|
|
39
|
+
return props;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=handle.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* see `core/README.md` for why this package is split the way it is.
|
|
5
|
-
|
|
6
|
-
* Archived provenance rationale:
|
|
7
|
-
* ## The DOM split (why this package has two entry points)
|
|
8
|
-
*
|
|
9
|
-
* Everything under `src/` except `src/dom/` compiles under `tsconfig.json`, which extends the repo's
|
|
10
|
-
* `tsconfig.base.json` unmodified (`lib: ["ES2023"]`, no `DOM`) and **excludes `src/dom` entirely**.
|
|
11
|
-
* `src/dom/` compiles separately under `tsconfig.dom.json`, the only config in this package with
|
|
12
|
-
* `DOM`/`DOM.Iterable` in `lib`.
|
|
13
|
-
*
|
|
14
|
-
* This is a compile-time guarantee, not a convention: a `document`/`window` reference anywhere
|
|
15
|
-
* outside `src/dom/**` fails `tsc -p tsconfig.json` with "Cannot find name 'document'" (verified
|
|
16
|
-
* during the 2026-07-26 extraction — see the extraction's report for the exact command and error
|
|
17
|
-
* text). That is what proves the policy layer (`page-executor.ts`'s guards, refusals, and
|
|
18
|
-
* allowlist-enforcement) cannot quietly reach into a live page; only `src/dom/dom-page-driver.ts`,
|
|
19
|
-
* the one `PageDriver` implementation that legitimately needs a DOM, may.
|
|
20
|
-
*
|
|
21
|
-
* Two `package.json` exports follow the same split:
|
|
22
|
-
*
|
|
23
|
-
* - `.` (`dist/index.d.ts`/`dist/index.js`, built by `tsconfig.json`) — the DOM-free vocabulary,
|
|
24
|
-
* policy gate, and protocol projections. What `@jini/chat-core`, `@jini/daemon` (structurally, see
|
|
25
|
-
* below), and (once §5 of the plan lands) `@jini/ui` depend on.
|
|
26
|
-
* - `./dom` (`dist/dom/index.d.ts`/`dist/dom/index.js`, built by `tsconfig.dom.json`) — the browser
|
|
27
|
-
* `PageDriver`. What `@jini/chat-react` depends on.
|
|
28
|
-
*
|
|
29
|
-
* `scripts/check-engine-boundaries.ts` R2 normally allows only bare `@jini/<name>` imports (one
|
|
30
|
-
* entry point per package), with a single named exception for `@jini/core/internal`. This extraction
|
|
31
|
-
* added a second, identically-gated exception for the exact literal `@jini/agentic/dom` — not a
|
|
32
|
-
* pattern — because `@jini/chat-react` genuinely needs the DOM half and there is no third package for
|
|
33
|
-
* it to live in without recreating the sprawl this plan exists to reduce (23 packages vs. a locked
|
|
34
|
-
* 14). See that script's own module doc for the up-to-date rule list.
|
|
35
|
-
*/
|
|
1
|
+
/** Compatibility root: preserve existing imports by re-exporting core. Prefer the explicit
|
|
2
|
+
* @jini-ai/agentic/core entry for new code. DOM-free policy is compiled without browser globals;
|
|
3
|
+
* only core/dom owns the PageDriver that touches a live page. See core/README.md for the split. */
|
|
36
4
|
export * from './core/index.js';
|
|
37
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* see `core/README.md` for why this package is split the way it is.
|
|
5
|
-
|
|
6
|
-
* Archived provenance rationale:
|
|
7
|
-
* ## The DOM split (why this package has two entry points)
|
|
8
|
-
*
|
|
9
|
-
* Everything under `src/` except `src/dom/` compiles under `tsconfig.json`, which extends the repo's
|
|
10
|
-
* `tsconfig.base.json` unmodified (`lib: ["ES2023"]`, no `DOM`) and **excludes `src/dom` entirely**.
|
|
11
|
-
* `src/dom/` compiles separately under `tsconfig.dom.json`, the only config in this package with
|
|
12
|
-
* `DOM`/`DOM.Iterable` in `lib`.
|
|
13
|
-
*
|
|
14
|
-
* This is a compile-time guarantee, not a convention: a `document`/`window` reference anywhere
|
|
15
|
-
* outside `src/dom/**` fails `tsc -p tsconfig.json` with "Cannot find name 'document'" (verified
|
|
16
|
-
* during the 2026-07-26 extraction — see the extraction's report for the exact command and error
|
|
17
|
-
* text). That is what proves the policy layer (`page-executor.ts`'s guards, refusals, and
|
|
18
|
-
* allowlist-enforcement) cannot quietly reach into a live page; only `src/dom/dom-page-driver.ts`,
|
|
19
|
-
* the one `PageDriver` implementation that legitimately needs a DOM, may.
|
|
20
|
-
*
|
|
21
|
-
* Two `package.json` exports follow the same split:
|
|
22
|
-
*
|
|
23
|
-
* - `.` (`dist/index.d.ts`/`dist/index.js`, built by `tsconfig.json`) — the DOM-free vocabulary,
|
|
24
|
-
* policy gate, and protocol projections. What `@jini/chat-core`, `@jini/daemon` (structurally, see
|
|
25
|
-
* below), and (once §5 of the plan lands) `@jini/ui` depend on.
|
|
26
|
-
* - `./dom` (`dist/dom/index.d.ts`/`dist/dom/index.js`, built by `tsconfig.dom.json`) — the browser
|
|
27
|
-
* `PageDriver`. What `@jini/chat-react` depends on.
|
|
28
|
-
*
|
|
29
|
-
* `scripts/check-engine-boundaries.ts` R2 normally allows only bare `@jini/<name>` imports (one
|
|
30
|
-
* entry point per package), with a single named exception for `@jini/core/internal`. This extraction
|
|
31
|
-
* added a second, identically-gated exception for the exact literal `@jini/agentic/dom` — not a
|
|
32
|
-
* pattern — because `@jini/chat-react` genuinely needs the DOM half and there is no third package for
|
|
33
|
-
* it to live in without recreating the sprawl this plan exists to reduce (23 packages vs. a locked
|
|
34
|
-
* 14). See that script's own module doc for the up-to-date rule list.
|
|
35
|
-
*/
|
|
1
|
+
/** Compatibility root: preserve existing imports by re-exporting core. Prefer the explicit
|
|
2
|
+
* @jini-ai/agentic/core entry for new code. DOM-free policy is compiled without browser globals;
|
|
3
|
+
* only core/dom owns the PageDriver that touches a live page. See core/README.md for the split. */
|
|
36
4
|
export * from './core/index.js';
|
|
37
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module mcp-ui
|
|
3
|
+
*
|
|
4
|
+
* The MCP Apps (`mcp-ui`) envelope: JSON-RPC 2.0 over `postMessage` between a host and a
|
|
5
|
+
* sandboxed iframe View.
|
|
6
|
+
*
|
|
7
|
+
* MCP Apps became the first official MCP extension (SEP-1865) on 2026-01-26. Jini borrows its
|
|
8
|
+
* **envelope and lifecycle** rather than inventing a message format, because the hard parts are
|
|
9
|
+
* already settled there: request/response correlation by `id`, and the ordering rule that a host
|
|
10
|
+
* MUST NOT send any request or notification to a View before receiving `initialized` — which is
|
|
11
|
+
* the fix for the "posted before the iframe was listening" race.
|
|
12
|
+
*
|
|
13
|
+
* Re-verified 2026-07-28 against the primary source (`github.com/modelcontextprotocol/ext-apps`,
|
|
14
|
+
* both `specification/2026-01-26/apps.mdx` [Stable] and `specification/draft/apps.mdx`) after an
|
|
15
|
+
* audit found the bucket below had the wrong direction for two lifecycle messages and mis-typed a
|
|
16
|
+
* third as fire-and-forget when the spec defines it as a request needing a response:
|
|
17
|
+
*
|
|
18
|
+
* - `ui/notifications/initialized` and `ui/notifications/size-changed` are **View→Host** (the
|
|
19
|
+
* sequence diagram is unambiguous: `UI ->> H: ui/notifications/initialized`, and the "Host
|
|
20
|
+
* Behavior" prose for size reads "hosts MUST **listen for** ... notifications **from the
|
|
21
|
+
* View**"). They were previously bucketed under `MCP_UI_HOST_NOTIFICATIONS`, i.e. backwards —
|
|
22
|
+
* now under {@link MCP_UI_VIEW_NOTIFICATIONS}.
|
|
23
|
+
* - `ui/resource-teardown` (Host→View) carries an `id` and has success/error response shapes in
|
|
24
|
+
* the spec's own wire examples, and the prose is explicit: "Host SHOULD wait for a response
|
|
25
|
+
* before tearing down the resource (to prevent data loss)." That is a request, not a
|
|
26
|
+
* notification — moved to {@link MCP_UI_HOST_REQUESTS}.
|
|
27
|
+
*
|
|
28
|
+
* The draft spec (not yet Stable, but already fixing the above and adding surface this module
|
|
29
|
+
* now also models) adds: `ui/notifications/request-teardown` (View→Host, the View asking to be
|
|
30
|
+
* torn down), `ui/download-file` and `sampling/createMessage` (View→Host requests), and the
|
|
31
|
+
* Host→App direction of `tools/call`/`tools/list` for Apps that register their own tools (the
|
|
32
|
+
* "App-Provided Tools" section, declared via `appCapabilities.tools` in `ui/initialize`).
|
|
33
|
+
*
|
|
34
|
+
* Two deliberate divergences from the spec, both load-bearing:
|
|
35
|
+
*
|
|
36
|
+
* 1. **Direction.** MCP Apps is primarily View→Host: the embedded UI calls tools. Host→View is a
|
|
37
|
+
* *fixed* lifecycle set with no generic "host asks the View to do something" (the App-tools
|
|
38
|
+
* addition above is the one exception, and it is opt-in and capability-gated). Jini's page
|
|
39
|
+
* verbs therefore ride as an explicitly-namespaced extension ({@link JINI_PAGE_ACTION_METHOD})
|
|
40
|
+
* — ours, not the spec's, and named so no one mistakes it.
|
|
41
|
+
* 2. **Sandbox.** The spec mandates `sandbox="allow-scripts allow-same-origin"`, which is safe
|
|
42
|
+
* *there* because the View is served from a separate origin behind a sandbox proxy. A host
|
|
43
|
+
* serving preview content from its own origin must NOT copy that line: `allow-same-origin`
|
|
44
|
+
* would hand the embedded document access to the host's origin. See
|
|
45
|
+
* {@link MCP_UI_SANDBOX_NOTE}.
|
|
46
|
+
*
|
|
47
|
+
* No transport here — this package touches no browser globals. These are pure builders and
|
|
48
|
+
* parsers; the binding that owns the iframe does the posting.
|
|
49
|
+
*/
|
|
50
|
+
/** Requests the View may send the Host. From the 2026-01-26 [Stable] spec plus the draft's additions (marked below). */
|
|
51
|
+
export declare const MCP_UI_VIEW_METHODS: {
|
|
52
|
+
readonly initialize: "ui/initialize";
|
|
53
|
+
/** View→Host: call a server tool via the Host's proxy (distinct from {@link MCP_UI_HOST_REQUESTS.callAppTool}, the reverse direction). */
|
|
54
|
+
readonly callTool: "tools/call";
|
|
55
|
+
readonly readResource: "resources/read";
|
|
56
|
+
readonly log: "notifications/message";
|
|
57
|
+
readonly openLink: "ui/open-link";
|
|
58
|
+
readonly sendMessage: "ui/message";
|
|
59
|
+
readonly requestDisplayMode: "ui/request-display-mode";
|
|
60
|
+
readonly updateModelContext: "ui/update-model-context";
|
|
61
|
+
/** Draft addition. Host-mediated file download from a sandboxed iframe that cannot use `allow-downloads`. */
|
|
62
|
+
readonly downloadFile: "ui/download-file";
|
|
63
|
+
/** Draft addition. Request an LLM completion from the Host; gated behind `hostCapabilities.sampling`. */
|
|
64
|
+
readonly createMessage: "sampling/createMessage";
|
|
65
|
+
readonly ping: "ping";
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Notifications the Host may send the View — fire-and-forget, no response expected or allowed.
|
|
69
|
+
* `initialized`/`size-changed` are NOT here: both are sent BY the View (see
|
|
70
|
+
* {@link MCP_UI_VIEW_NOTIFICATIONS}), and `teardown` is NOT here either: it is a request the Host
|
|
71
|
+
* sends TO the View expecting a response (see {@link MCP_UI_HOST_REQUESTS}).
|
|
72
|
+
*/
|
|
73
|
+
export declare const MCP_UI_HOST_NOTIFICATIONS: {
|
|
74
|
+
readonly toolInputPartial: "ui/notifications/tool-input-partial";
|
|
75
|
+
readonly toolInput: "ui/notifications/tool-input";
|
|
76
|
+
readonly toolResult: "ui/notifications/tool-result";
|
|
77
|
+
readonly toolCancelled: "ui/notifications/tool-cancelled";
|
|
78
|
+
readonly hostContextChanged: "ui/notifications/host-context-changed";
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Notifications the View may send the Host — fire-and-forget, no response expected or allowed.
|
|
82
|
+
* The spec's own "Notifications (Host → View)" heading in the 2026-01-26 text is misleading for
|
|
83
|
+
* two of these (see the module doc's audit note); the draft spec corrects this by giving these
|
|
84
|
+
* two, plus its own new addition, an explicit "Notifications (View → Host)" heading.
|
|
85
|
+
*/
|
|
86
|
+
export declare const MCP_UI_VIEW_NOTIFICATIONS: {
|
|
87
|
+
/** The View→Host handshake completion notification. Host MUST NOT send anything to the View before this arrives. */
|
|
88
|
+
readonly initialized: "ui/notifications/initialized";
|
|
89
|
+
/** The View SHOULD send this "when rendered content body size changes." */
|
|
90
|
+
readonly sizeChanged: "ui/notifications/size-changed";
|
|
91
|
+
/** Draft addition. The View asking to be torn down (e.g. a "Done" button); the Host MAY defer or ignore it, but if it accepts, it still owes the View {@link MCP_UI_HOST_REQUESTS.teardown}. */
|
|
92
|
+
readonly requestTeardown: "ui/notifications/request-teardown";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Requests the Host may send the View, needing a response. Distinct from
|
|
96
|
+
* {@link MCP_UI_HOST_NOTIFICATIONS} because a caller building a JSON-RPC envelope for one of
|
|
97
|
+
* these must attach an `id` and correlate the reply — using {@link createJsonRpcNotification}
|
|
98
|
+
* here would silently produce a message the View has no `id` to answer, and a Host that then
|
|
99
|
+
* "waits for a response" (the spec's own words) would wait forever.
|
|
100
|
+
*/
|
|
101
|
+
export declare const MCP_UI_HOST_REQUESTS: {
|
|
102
|
+
/**
|
|
103
|
+
* Host MUST send this before tearing down the UI resource, for any reason. Per spec: "Host
|
|
104
|
+
* SHOULD wait for a response before tearing down the resource (to prevent data loss)." — this
|
|
105
|
+
* is the message the bug report flagged: bucketing it as a fire-and-forget notification means a
|
|
106
|
+
* Host never learns whether the View acknowledged before disappearing.
|
|
107
|
+
*/
|
|
108
|
+
readonly teardown: "ui/resource-teardown";
|
|
109
|
+
/** Draft addition ("App-Provided Tools"). Host→App: execute a tool the App registered via `appCapabilities.tools`. */
|
|
110
|
+
readonly callAppTool: "tools/call";
|
|
111
|
+
/** Draft addition. Host→App: list the App's registered tools. */
|
|
112
|
+
readonly listAppTools: "tools/list";
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Jini's page-action extension. **Not part of MCP Apps** — the namespace is explicit so it can
|
|
116
|
+
* never be mistaken for a spec method, and so a compliant host that does not understand it
|
|
117
|
+
* rejects it as unknown rather than silently mis-handling it.
|
|
118
|
+
*/
|
|
119
|
+
export declare const JINI_PAGE_ACTION_METHOD = "x-jini/page-action";
|
|
120
|
+
/**
|
|
121
|
+
* Why a host serving preview content from its own origin must not copy the spec's sandbox line.
|
|
122
|
+
* Kept as an exported string so the reason travels with the code that would otherwise be
|
|
123
|
+
* cargo-culted.
|
|
124
|
+
*/
|
|
125
|
+
export declare const MCP_UI_SANDBOX_NOTE: string;
|
|
126
|
+
export interface JsonRpcRequest {
|
|
127
|
+
readonly jsonrpc: '2.0';
|
|
128
|
+
readonly id: number | string;
|
|
129
|
+
readonly method: string;
|
|
130
|
+
readonly params?: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
export interface JsonRpcNotification {
|
|
133
|
+
readonly jsonrpc: '2.0';
|
|
134
|
+
readonly method: string;
|
|
135
|
+
readonly params?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
export interface JsonRpcError {
|
|
138
|
+
readonly code: number;
|
|
139
|
+
readonly message: string;
|
|
140
|
+
readonly data?: unknown;
|
|
141
|
+
}
|
|
142
|
+
export interface JsonRpcResponse {
|
|
143
|
+
readonly jsonrpc: '2.0';
|
|
144
|
+
readonly id: number | string;
|
|
145
|
+
readonly result?: unknown;
|
|
146
|
+
readonly error?: JsonRpcError;
|
|
147
|
+
}
|
|
148
|
+
export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
|
|
149
|
+
/** JSON-RPC 2.0 reserved codes, used so a View's failures read the same as any other client's. */
|
|
150
|
+
export declare const JSON_RPC_ERROR_CODES: {
|
|
151
|
+
readonly parseError: -32700;
|
|
152
|
+
readonly invalidRequest: -32600;
|
|
153
|
+
readonly methodNotFound: -32601;
|
|
154
|
+
readonly invalidParams: -32602;
|
|
155
|
+
readonly internalError: -32603;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Whether an arbitrary value is a JSON-RPC 2.0 message.
|
|
159
|
+
*
|
|
160
|
+
* Messages arriving over `postMessage` are attacker-influenced input — any page able to reach the
|
|
161
|
+
* frame can post anything. Validate before dispatch; never trust the shape.
|
|
162
|
+
*
|
|
163
|
+
* Found by live adversarial testing against a real sandboxed iframe (2026-07-28), NOT by static
|
|
164
|
+
* review: a request whose `method` is a valid string but whose `id` is present and malformed
|
|
165
|
+
* (e.g. an object, `{ jsonrpc: '2.0', id: { nested: true }, method: 'tools/call' }`) used to pass
|
|
166
|
+
* this check — the `typeof value['method'] === 'string'` branch returned `true` unconditionally,
|
|
167
|
+
* without ever looking at `id`'s type. A Host trusting that then had to build a JSON-RPC response
|
|
168
|
+
* carrying a non-spec `id` back at the View, which the View's own correlation map (keyed by
|
|
169
|
+
* string ids) could never match — not a crash, but a real protocol violation this function exists
|
|
170
|
+
* specifically to catch. Now: if `id` is present at all, its type is checked before anything else,
|
|
171
|
+
* for every message shape (request, notification, or response) — a notification legitimately has
|
|
172
|
+
* no `id`, but a message that HAS one must have a valid one.
|
|
173
|
+
*
|
|
174
|
+
* The 2026-07-29 audit found the rest of that check was still far too generous, in three ways, all
|
|
175
|
+
* of them reachable from anything able to post at the frame:
|
|
176
|
+
*
|
|
177
|
+
* - **`params` went unread.** Any string `method` returned `true` immediately, so
|
|
178
|
+
* `{method: 'tools/call', params: 'nope'}` reached dispatch and a handler reading
|
|
179
|
+
* `params.capabilityId` got `undefined` rather than a refusal. §4.2 fixes the type: Array or
|
|
180
|
+
* Object.
|
|
181
|
+
* - **The four shapes were not kept apart.** JSON-RPC 2.0 defines request, notification, success
|
|
182
|
+
* response and error response as distinct, mutually exclusive objects. A message carrying both
|
|
183
|
+
* `method` and `result` passed as a request; one carrying both `result` and `error` passed as a
|
|
184
|
+
* response, which §5 forbids in as many words ("Either the result member or error member MUST be
|
|
185
|
+
* included, but both members MUST NOT be included").
|
|
186
|
+
* - **`error` was accepted as any value at all**, `error: "boom"` included, so a host formatting
|
|
187
|
+
* `error.message` back to a user read a property off a string.
|
|
188
|
+
*
|
|
189
|
+
* A message whose `method` is present but not a string is rejected outright rather than falling
|
|
190
|
+
* through to the response branch — reinterpreting a malformed request as a response is how one
|
|
191
|
+
* bad message becomes two wrong behaviors.
|
|
192
|
+
*
|
|
193
|
+
* @param value - The raw `event.data`.
|
|
194
|
+
* @returns True when the value is a well-formed JSON-RPC message.
|
|
195
|
+
*/
|
|
196
|
+
export declare function isJsonRpcMessage(value: unknown): value is JsonRpcMessage;
|
|
197
|
+
/** Whether a validated message expects a response (as opposed to being a notification). */
|
|
198
|
+
export declare function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcRequest;
|
|
199
|
+
/**
|
|
200
|
+
* Builds a request.
|
|
201
|
+
*
|
|
202
|
+
* @param id - Correlation id, unique per in-flight request. Doubles as the idempotency key: a
|
|
203
|
+
* transport that redelivers (an SSE reconnect, a repeated post) must not execute twice.
|
|
204
|
+
* @param method - Method name.
|
|
205
|
+
* @param params - Optional parameters.
|
|
206
|
+
*/
|
|
207
|
+
export declare function createJsonRpcRequest(id: number | string, method: string, params?: Record<string, unknown>): JsonRpcRequest;
|
|
208
|
+
/** Builds a notification — no `id`, so no response is expected or allowed. */
|
|
209
|
+
export declare function createJsonRpcNotification(method: string, params?: Record<string, unknown>): JsonRpcNotification;
|
|
210
|
+
/** Builds a success response for `id`. */
|
|
211
|
+
export declare function createJsonRpcResult(id: number | string, result: unknown): JsonRpcResponse;
|
|
212
|
+
/** Builds an error response for `id`. */
|
|
213
|
+
export declare function createJsonRpcError(id: number | string, code: number, message: string, data?: unknown): JsonRpcResponse;
|
|
214
|
+
/**
|
|
215
|
+
* Builds a Jini page-action request.
|
|
216
|
+
*
|
|
217
|
+
* @param id - Correlation/idempotency id.
|
|
218
|
+
* @param capabilityId - A `page.*` capability id.
|
|
219
|
+
* @param input - Capability arguments.
|
|
220
|
+
* @returns The namespaced extension request.
|
|
221
|
+
*/
|
|
222
|
+
export declare function createPageActionRequest(id: number | string, capabilityId: string, input: Record<string, unknown>): JsonRpcRequest;
|
|
223
|
+
//# sourceMappingURL=mcp-ui-apps.d.ts.map
|