@jini-ai/agentic 0.4.3 → 0.4.5

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/a2ui/json-pointer.js +94 -73
  3. package/dist/core/a2ui/resolve.js +45 -23
  4. package/dist/core/a2ui/tree.js +8 -5
  5. package/dist/core/capability.js +21 -12
  6. package/dist/core/dom/dom-page-driver.js +89 -74
  7. package/dist/core/gen-ui/encoder.js +31 -31
  8. package/dist/core/guards.js +13 -6
  9. package/dist/core/handle.d.ts +13 -0
  10. package/dist/core/handle.js +19 -0
  11. package/dist/core/index.d.ts +2 -1
  12. package/dist/core/index.js +2 -1
  13. package/dist/core/mcp-ui-apps.js +12 -6
  14. package/dist/core/page-executor.js +131 -111
  15. package/dist/core/site-guide/index.d.ts +4 -0
  16. package/dist/core/site-guide/index.js +4 -0
  17. package/dist/core/site-guide/site-guide.d.ts +18 -0
  18. package/dist/core/site-guide/site-guide.js +162 -0
  19. package/dist/core/site-guide/topic-schema.d.ts +16 -0
  20. package/dist/core/site-guide/topic-schema.js +68 -0
  21. package/dist/core/site-guide/types.d.ts +83 -0
  22. package/dist/core/site-guide/types.js +2 -0
  23. package/dist/skills/install/archive.js +42 -30
  24. package/dist/skills/install/github.js +102 -63
  25. package/dist/skills/install/index.d.ts +1 -0
  26. package/dist/skills/install/index.js +1 -0
  27. package/dist/skills/install/install-service.d.ts +20 -1
  28. package/dist/skills/install/install-service.js +23 -11
  29. package/dist/skills/install/layout.d.ts +8 -0
  30. package/dist/skills/install/layout.js +26 -1
  31. package/dist/skills/install/live-registration.js +1 -0
  32. package/dist/skills/install/node-filesystem.js +3 -1
  33. package/dist/skills/install/state.js +18 -7
  34. package/dist/skills/install/validation.d.ts +5 -1
  35. package/dist/skills/install/validation.js +85 -52
  36. package/dist/skills/install/yauzl-archive-reader.d.ts +29 -0
  37. package/dist/skills/install/yauzl-archive-reader.js +31 -0
  38. package/package.json +4 -4
  39. package/dist/a2ui/agent-to-renderer.d.ts +0 -613
  40. package/dist/a2ui/agent-to-renderer.js +0 -191
  41. package/dist/a2ui/catalog.d.ts +0 -73
  42. package/dist/a2ui/catalog.js +0 -304
  43. package/dist/a2ui/common-types.d.ts +0 -401
  44. package/dist/a2ui/common-types.js +0 -130
  45. package/dist/a2ui/index.d.ts +0 -16
  46. package/dist/a2ui/index.js +0 -16
  47. package/dist/a2ui/interpreter.d.ts +0 -71
  48. package/dist/a2ui/interpreter.js +0 -337
  49. package/dist/a2ui/json-pointer.d.ts +0 -48
  50. package/dist/a2ui/json-pointer.js +0 -174
  51. package/dist/a2ui/renderer-to-agent.d.ts +0 -342
  52. package/dist/a2ui/renderer-to-agent.js +0 -111
  53. package/dist/a2ui/resolve.d.ts +0 -28
  54. package/dist/a2ui/resolve.js +0 -144
  55. package/dist/a2ui/tree.d.ts +0 -66
  56. package/dist/a2ui/tree.js +0 -74
  57. package/dist/ag-ui.d.ts +0 -82
  58. package/dist/ag-ui.js +0 -85
  59. package/dist/agui/encoder.d.ts +0 -50
  60. package/dist/agui/encoder.js +0 -97
  61. package/dist/agui/events.d.ts +0 -59
  62. package/dist/agui/events.js +0 -15
  63. package/dist/capability.d.ts +0 -81
  64. package/dist/capability.js +0 -79
  65. package/dist/dom/dom-page-driver.d.ts +0 -36
  66. package/dist/dom/dom-page-driver.js +0 -601
  67. package/dist/dom/index.d.ts +0 -17
  68. package/dist/dom/index.js +0 -17
  69. package/dist/dom/model-context.d.ts +0 -69
  70. package/dist/dom/model-context.js +0 -44
  71. package/dist/element-handles.d.ts +0 -120
  72. package/dist/element-handles.js +0 -39
  73. package/dist/gen-ui/encoder.d.ts +0 -50
  74. package/dist/gen-ui/encoder.js +0 -97
  75. package/dist/gen-ui/events.d.ts +0 -59
  76. package/dist/gen-ui/events.js +0 -15
  77. package/dist/gen-ui/index.d.ts +0 -18
  78. package/dist/gen-ui/index.js +0 -17
  79. package/dist/guards.d.ts +0 -96
  80. package/dist/guards.js +0 -186
  81. package/dist/handle.d.ts +0 -41
  82. package/dist/handle.js +0 -41
  83. package/dist/mcp-ui-apps.d.ts +0 -223
  84. package/dist/mcp-ui-apps.js +0 -252
  85. package/dist/mcp-ui.d.ts +0 -133
  86. package/dist/mcp-ui.js +0 -134
  87. package/dist/page-capabilities.d.ts +0 -14
  88. package/dist/page-capabilities.js +0 -129
  89. package/dist/page-driver.d.ts +0 -120
  90. package/dist/page-driver.js +0 -2
  91. package/dist/page-executor.d.ts +0 -94
  92. package/dist/page-executor.js +0 -320
  93. package/dist/webmcp.d.ts +0 -202
  94. package/dist/webmcp.js +0 -173
@@ -1,3 +1,13 @@
1
+ /** `stage_start` / `stage_end` share one shape; `iteration` is omitted entirely (not set to `undefined`) when absent. */
2
+ function encodeStage(payload, base) {
3
+ return {
4
+ ...base,
5
+ kind: 'run.lifecycle',
6
+ status: payload.type === 'stage_start' ? 'pipeline_stage_started' : 'pipeline_stage_completed',
7
+ stageId: payload.stageId,
8
+ ...(payload.iteration !== undefined ? { iteration: payload.iteration } : {}),
9
+ };
10
+ }
1
11
  /**
2
12
  * Creates a fresh `GenUiEncoder` with its own, empty tool-call correlation map.
3
13
  * @complexity `encode` is O(1) per call (one `Map` get/set/delete); memory is O(k) in the number
@@ -10,6 +20,24 @@ export function createGenUiEncoder({ clock }, _optional = {}) {
10
20
  const ts = ctx.now ? ctx.now({}) : clock.nowMs();
11
21
  return ctx.seq !== undefined ? { runId: ctx.runId, ts, seq: ctx.seq } : { runId: ctx.runId, ts };
12
22
  }
23
+ /** Resolves a `tool_result` against (and removes) its correlated `tool_use`. */
24
+ function encodeToolResult(payload, base) {
25
+ const pending = pendingToolCalls.get(payload.toolUseId);
26
+ pendingToolCalls.delete(payload.toolUseId);
27
+ return {
28
+ ...base,
29
+ kind: 'tool_call',
30
+ // A tool_result with no matching prior tool_use (never observed, or already resolved/
31
+ // cleared — e.g. after a run-ending 'end' event, see below) falls back to an 'unknown'
32
+ // name and null args rather than throwing: the result itself is still real and worth
33
+ // relaying even when its originating call wasn't captured.
34
+ toolName: pending?.toolName ?? 'unknown',
35
+ args: pending?.args ?? null,
36
+ callId: payload.toolUseId,
37
+ status: payload.isError ? 'failed' : 'completed',
38
+ result: payload.content,
39
+ };
40
+ }
13
41
  function encodeAgentPayload(payload, ctx) {
14
42
  const base = baseFields(ctx);
15
43
  switch (payload.type) {
@@ -21,42 +49,14 @@ export function createGenUiEncoder({ clock }, _optional = {}) {
21
49
  // and there is no protocol-level guarantee ids are never reused within one run.
22
50
  pendingToolCalls.set(payload.id, { toolName: payload.name, args: payload.input });
23
51
  return { ...base, kind: 'tool_call', toolName: payload.name, args: payload.input, callId: payload.id, status: 'started' };
24
- case 'tool_result': {
25
- const pending = pendingToolCalls.get(payload.toolUseId);
26
- pendingToolCalls.delete(payload.toolUseId);
27
- return {
28
- ...base,
29
- kind: 'tool_call',
30
- // A tool_result with no matching prior tool_use (never observed, or already resolved/
31
- // cleared — e.g. after a run-ending 'end' event, see below) falls back to an 'unknown'
32
- // name and null args rather than throwing: the result itself is still real and worth
33
- // relaying even when its originating call wasn't captured.
34
- toolName: pending?.toolName ?? 'unknown',
35
- args: pending?.args ?? null,
36
- callId: payload.toolUseId,
37
- status: payload.isError ? 'failed' : 'completed',
38
- result: payload.content,
39
- };
40
- }
52
+ case 'tool_result':
53
+ return encodeToolResult(payload, base);
41
54
  // Generalization (see archived provenance ledger): pipeline_stage_started/completed generalized into a
42
55
  // named-stage boundary any multi-step driver can emit, flowing through the same 'agent'
43
56
  // channel as tool_use/tool_result.
44
57
  case 'stage_start':
45
- return {
46
- ...base,
47
- kind: 'run.lifecycle',
48
- status: 'pipeline_stage_started',
49
- stageId: payload.stageId,
50
- ...(payload.iteration !== undefined ? { iteration: payload.iteration } : {}),
51
- };
52
58
  case 'stage_end':
53
- return {
54
- ...base,
55
- kind: 'run.lifecycle',
56
- status: 'pipeline_stage_completed',
57
- stageId: payload.stageId,
58
- ...(payload.iteration !== undefined ? { iteration: payload.iteration } : {}),
59
- };
59
+ return encodeStage(payload, base);
60
60
  // Generalization (see archived provenance ledger): genui_surface_request/response generalized into a
61
61
  // generic human-in-the-loop ask/answer pair.
62
62
  case 'surface_request':
@@ -81,6 +81,13 @@ export function findFieldReadRefusal({ field }, _optional = {}) {
81
81
  const type = field.type?.toLowerCase();
82
82
  if (type !== undefined && DENIED_TYPES.has(type))
83
83
  return 'denied-type';
84
+ if (hasDeniedAutocomplete({ field }))
85
+ return 'denied-autocomplete';
86
+ if (hasSuspiciousFieldName({ field }))
87
+ return 'suspicious-name';
88
+ return null;
89
+ }
90
+ function hasDeniedAutocomplete({ field }, _optional = {}) {
84
91
  const autocomplete = field.autocomplete?.toLowerCase();
85
92
  if (autocomplete !== undefined) {
86
93
  // The real grammar is space-separated tokens, but a comma-joined value
@@ -90,10 +97,13 @@ export function findFieldReadRefusal({ field }, _optional = {}) {
90
97
  // worked as a plain text input while looking unguarded.
91
98
  for (const token of autocomplete.split(/[\s,]+/)) {
92
99
  if (DENIED_AUTOCOMPLETE.has(token))
93
- return 'denied-autocomplete';
100
+ return true;
94
101
  }
95
102
  }
96
- if (SUSPICIOUS_NAME.test(squashSeparators(field.name ?? ''))
103
+ return false;
104
+ }
105
+ function hasSuspiciousFieldName({ field }, _optional = {}) {
106
+ return (SUSPICIOUS_NAME.test(squashSeparators(field.name ?? ''))
97
107
  || SUSPICIOUS_NAME.test(squashSeparators(field.id ?? ''))
98
108
  // `name`/`id` are the *machine* names; a form builder or CMS that emits `name="field_47"`
99
109
  // next to a visible "Card number" label left the guard nothing to judge until this existed.
@@ -102,10 +112,7 @@ export function findFieldReadRefusal({ field }, _optional = {}) {
102
112
  // deletes the separators, so a join would let two innocuous labels form a trigger word across
103
113
  // their boundary ("Address" + "Snapshot" -> "addresssnapshot", which contains "ssn"). This
104
114
  // 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;
115
+ || (field.accessibleLabels ?? []).some((label) => SUSPICIOUS_NAME.test(squashSeparators(label))));
109
116
  }
110
117
  /**
111
118
  * Why an automated fill must be refused for this field, or `null` when it is safe to write.
@@ -40,6 +40,19 @@ export type AgentHandleProps = {
40
40
  export declare function agentHandle({ handle }: {
41
41
  handle: string;
42
42
  }, options?: AgentHandleOptions): AgentHandleProps;
43
+ /**
44
+ * Derives a bounded handle segment from an opaque id without rewriting that id.
45
+ * Short valid ids keep their spelling; other ids use a stable 64-bit FNV-1a fingerprint
46
+ * of their UTF-16 code units. The `h-<16 hex digits>` namespace is reserved for fingerprints.
47
+ * Unlike list collision suffixes, this segment does not depend on filtering or row order.
48
+ * This is display identity, not a cryptographic or authorization primitive.
49
+ * @param required - The original id; arbitrary characters, empty and long strings are accepted.
50
+ * @returns A valid segment of at most 40 characters; callers reserve space for their fixed prefix.
51
+ * @complexity O(n) time in id characters, O(1) auxiliary space. No effects or errors.
52
+ */
53
+ export declare function agentHandleSegment({ value }: {
54
+ value: string;
55
+ }, _optional?: Record<string, never>): string;
43
56
  /**
44
57
  * The handle for one sub-element published under `base` — `<base>-<action>`.
45
58
  *
@@ -38,6 +38,25 @@ export function agentHandle({ handle }, options = {}) {
38
38
  props[AGENT_PAGE_ATTRIBUTE] = options.page;
39
39
  return props;
40
40
  }
41
+ /**
42
+ * Derives a bounded handle segment from an opaque id without rewriting that id.
43
+ * Short valid ids keep their spelling; other ids use a stable 64-bit FNV-1a fingerprint
44
+ * of their UTF-16 code units. The `h-<16 hex digits>` namespace is reserved for fingerprints.
45
+ * Unlike list collision suffixes, this segment does not depend on filtering or row order.
46
+ * This is display identity, not a cryptographic or authorization primitive.
47
+ * @param required - The original id; arbitrary characters, empty and long strings are accepted.
48
+ * @returns A valid segment of at most 40 characters; callers reserve space for their fixed prefix.
49
+ * @complexity O(n) time in id characters, O(1) auxiliary space. No effects or errors.
50
+ */
51
+ export function agentHandleSegment({ value }, _optional = {}) {
52
+ if (value.length <= 40 && isValidElementHandle({ handle: value }) && !/^h-[0-9a-f]{16}$/.test(value))
53
+ return value;
54
+ let hash = 0xcbf29ce484222325n;
55
+ for (let index = 0; index < value.length; index += 1) {
56
+ hash = BigInt.asUintN(64, (hash ^ BigInt(value.charCodeAt(index))) * 0x100000001b3n);
57
+ }
58
+ return `h-${hash.toString(16).padStart(16, '0')}`;
59
+ }
41
60
  /**
42
61
  * The handle for one sub-element published under `base` — `<base>-<action>`.
43
62
  *
@@ -21,7 +21,7 @@ export type { PageDriver, FindElementsFilter, PageSummary } from './page-driver.
21
21
  export { executePageCapability, projectElementState, DEFAULT_HIGHLIGHT_MS, MAX_HIGHLIGHT_MS, MAX_STATEFUL_ELEMENTS, type FindElementsResult, type PageElementResult, type PageActivitySnapshot, type PageWriteObservation, } from './page-executor.js';
22
22
  export { PAGE_CAPABILITIES } from './page-capabilities.js';
23
23
  export { AGENT_ELEMENT_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_PRIVATE_ATTRIBUTE, AGENT_ELEMENT_ROLES, isValidElementHandle, resolveHandleSelector, type AgentElementRole, type AgentElementDescriptor, type AgentElementRawState, type AgentElementState, } from './element-handles.js';
24
- export { agentHandle, agentSubHandle, agentHandleProps, type AgentHandleOptions, type AgentHandleProps, type AgentHandlePropsOptions, } from './handle.js';
24
+ export { agentHandle, agentHandleSegment, agentSubHandle, agentHandleProps, type AgentHandleOptions, type AgentHandleProps, type AgentHandlePropsOptions, } from './handle.js';
25
25
  export { buildAgentListHandles } from './list-handles.js';
26
26
  export { findFieldFillRefusal, findFieldReadRefusal, describeFieldRefusal, describeFieldReadRefusal, normalizeAgentLabel, MAX_AGENT_LABEL_LENGTH, type FieldDescriptor, type FieldRefusal, type FieldReadRefusal, type NormalizedLabel, } from './guards.js';
27
27
  /**
@@ -48,4 +48,5 @@ export { createGenUiEncoder, type GenUiEncodeContext, type GenUiEncoder, type Ge
48
48
  export { MCP_UI_VIEW_METHODS, MCP_UI_HOST_NOTIFICATIONS, MCP_UI_VIEW_NOTIFICATIONS, MCP_UI_HOST_REQUESTS, MCP_UI_SANDBOX_NOTE, JINI_PAGE_ACTION_METHOD, JSON_RPC_ERROR_CODES, isJsonRpcMessage, isJsonRpcRequest, createJsonRpcRequest, createJsonRpcNotification, createJsonRpcResult, createJsonRpcError, createPageActionRequest, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcNotification, type JsonRpcResponse, type JsonRpcError, } from './mcp-ui-apps.js';
49
49
  export { defaultAgenticMessages } from "./messages.js";
50
50
  export { createAnnotatedActionsScript } from './annotated-actions-script.js';
51
+ export * from './site-guide/index.js';
51
52
  //# sourceMappingURL=index.d.ts.map
@@ -20,7 +20,7 @@ export { findCapability, findCapabilityInputError, availableCapabilities, } from
20
20
  export { executePageCapability, projectElementState, DEFAULT_HIGHLIGHT_MS, MAX_HIGHLIGHT_MS, MAX_STATEFUL_ELEMENTS, } from './page-executor.js';
21
21
  export { PAGE_CAPABILITIES } from './page-capabilities.js';
22
22
  export { AGENT_ELEMENT_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_PRIVATE_ATTRIBUTE, AGENT_ELEMENT_ROLES, isValidElementHandle, resolveHandleSelector, } from './element-handles.js';
23
- export { agentHandle, agentSubHandle, agentHandleProps, } from './handle.js';
23
+ export { agentHandle, agentHandleSegment, agentSubHandle, agentHandleProps, } from './handle.js';
24
24
  export { buildAgentListHandles } from './list-handles.js';
25
25
  export { findFieldFillRefusal, findFieldReadRefusal, describeFieldRefusal, describeFieldReadRefusal, normalizeAgentLabel, MAX_AGENT_LABEL_LENGTH, } from './guards.js';
26
26
  /**
@@ -47,4 +47,5 @@ export { createGenUiEncoder, } from './gen-ui/index.js';
47
47
  export { MCP_UI_VIEW_METHODS, MCP_UI_HOST_NOTIFICATIONS, MCP_UI_VIEW_NOTIFICATIONS, MCP_UI_HOST_REQUESTS, MCP_UI_SANDBOX_NOTE, JINI_PAGE_ACTION_METHOD, JSON_RPC_ERROR_CODES, isJsonRpcMessage, isJsonRpcRequest, createJsonRpcRequest, createJsonRpcNotification, createJsonRpcResult, createJsonRpcError, createPageActionRequest, } from './mcp-ui-apps.js';
48
48
  export { defaultAgenticMessages } from "./messages.js";
49
49
  export { createAnnotatedActionsScript } from './annotated-actions-script.js';
50
+ export * from './site-guide/index.js';
50
51
  //# sourceMappingURL=index.js.map
@@ -194,14 +194,10 @@ export function isJsonRpcMessage(required, _optional = {}) {
194
194
  const { value } = required;
195
195
  if (!isRecord(value) || value['jsonrpc'] !== '2.0')
196
196
  return false;
197
- if ('id' in value && typeof value['id'] !== 'number' && typeof value['id'] !== 'string')
197
+ if (!hasValidJsonRpcId({ value }))
198
198
  return false;
199
199
  if ('method' in value) {
200
- // Request or notification: `id` present or not, but never a response's members alongside.
201
- return typeof value['method'] === 'string'
202
- && hasValidParams(value)
203
- && !('result' in value)
204
- && !('error' in value);
200
+ return isJsonRpcCallShape({ value });
205
201
  }
206
202
  // Response: correlated by `id`, carrying exactly one of `result` / `error`.
207
203
  if (!('id' in value))
@@ -212,6 +208,16 @@ export function isJsonRpcMessage(required, _optional = {}) {
212
208
  return false;
213
209
  return hasResult || isJsonRpcErrorObject(value['error']);
214
210
  }
211
+ function hasValidJsonRpcId({ value }, _optional = {}) {
212
+ return !('id' in value) || typeof value['id'] === 'number' || typeof value['id'] === 'string';
213
+ }
214
+ function isJsonRpcCallShape({ value }, _optional = {}) {
215
+ // Request or notification: `id` present or not, but never a response's members alongside.
216
+ return typeof value['method'] === 'string'
217
+ && hasValidParams(value)
218
+ && !('result' in value)
219
+ && !('error' in value);
220
+ }
215
221
  /** Whether a validated message expects a response (as opposed to being a notification). */
216
222
  export function isJsonRpcRequest(required, _optional = {}) {
217
223
  const { message } = required;
@@ -11,7 +11,7 @@
11
11
  * Nothing here touches the DOM. That is what makes the interesting half testable without a
12
12
  * browser: "does it refuse to type into a password box" is a unit test, not an integration test.
13
13
  */
14
- import { findCapabilityInputError, } from './capability.js';
14
+ import { findCapability, findCapabilityInputError, } from './capability.js';
15
15
  import { AGENT_ELEMENT_ROLES, isValidElementHandle, } from './element-handles.js';
16
16
  import { describeFieldReadRefusal, describeFieldRefusal, findFieldFillRefusal, findFieldReadRefusal, normalizeAgentLabel, } from './guards.js';
17
17
  import { PAGE_CAPABILITIES } from './page-capabilities.js';
@@ -69,19 +69,28 @@ export function projectElementState({ raw }, _optional = {}) {
69
69
  // what the option is called; withholding the label too would blind a caller to the page's own
70
70
  // ontology for no reduction in what actually leaks.
71
71
  const refusal = raw.field !== undefined ? findFieldReadRefusal({ field: raw.field }) : null;
72
+ const withheldText = findWithheldText({ raw, refusal });
73
+ const base = {
74
+ text: withheldText === undefined ? text.text : '',
75
+ textTruncated: withheldText === undefined ? text.truncated : false,
76
+ ...(withheldText !== undefined ? { textWithheld: withheldText } : {}),
77
+ ...projectOptionalState({ raw, refusal }),
78
+ };
79
+ return projectFieldValue({ raw, refusal, base });
80
+ }
81
+ function findWithheldText({ raw, refusal }, _optional = {}) {
72
82
  // The one element whose `text` is NOT page-authored chrome: an editable region, whose contents
73
83
  // are the user's data and whose driver has no `value` to report them as. It is gated by exactly
74
84
  // the rules `value` is gated by below, including the "arrived with nothing to check it against"
75
85
  // one — an unverifiable secret is withheld, not trusted.
76
- const withheldText = raw.textIsValue !== true
86
+ return raw.textIsValue !== true
77
87
  ? undefined
78
88
  : raw.field === undefined
79
89
  ? 'this element reported contents without the attributes needed to check them for secrets'
80
90
  : refusal !== null ? describeFieldReadRefusal({ refusal }) : undefined;
81
- const base = {
82
- text: withheldText === undefined ? text.text : '',
83
- textTruncated: withheldText === undefined ? text.truncated : false,
84
- ...(withheldText !== undefined ? { textWithheld: withheldText } : {}),
91
+ }
92
+ function projectOptionalState({ raw, refusal }, _optional = {}) {
93
+ return {
85
94
  ...(raw.checked !== undefined && refusal === null ? { checked: raw.checked } : {}),
86
95
  ...(raw.disabled !== undefined ? { disabled: raw.disabled } : {}),
87
96
  ...(raw.visible !== undefined ? { visible: raw.visible } : {}),
@@ -91,6 +100,8 @@ export function projectElementState({ raw }, _optional = {}) {
91
100
  ? { options: raw.options.map((option) => normalizeAgentLabel({ raw: option }).text) }
92
101
  : {}),
93
102
  };
103
+ }
104
+ function projectFieldValue({ raw, refusal, base }, _optional = {}) {
94
105
  if (raw.value === undefined)
95
106
  return base;
96
107
  if (raw.field === undefined) {
@@ -176,66 +187,10 @@ async function summarizePage(driver) {
176
187
  * purpose: a caller must be told why, not handed a silent no-op it will retry forever.
177
188
  */
178
189
  export async function executePageCapability({ driver, capabilityId, input }, _optional = {}) {
179
- const capability = PAGE_CAPABILITIES.find((entry) => entry.id === capabilityId);
180
- if (capability === undefined)
181
- throw new Error(`unknown page capability: ${capabilityId}`);
182
- const inputError = findCapabilityInputError({ capability, input });
183
- if (inputError !== null) {
184
- // The schema rides along with the refusal rather than making a caller spend a second
185
- // round trip on describe_tool to learn what it already tried to guess — ai-control-plane.md
186
- // §29.4's "schema-on-error" requirement, empirically forced by a real run: page.fill was
187
- // called with `handle` instead of the argument's old name, `element`, twice before the caller
188
- // gave up and looked the schema up separately. One JSON-Schema object, embedded once, turned
189
- // that into a same-turn self-correction for every page.* verb this gate guards, not just the
190
- // one that got measured — self-correction is why that mismatch was survivable at all, given
191
- // every verb here needs a handle from `page.find_elements`, whose own response has always
192
- // called this same value `handle` (`AgentElementDescriptor.handle`). The argument itself was
193
- // later renamed from `element` to `handle` to match, closing the gap this comment describes
194
- // rather than merely papering over it turn by turn.
195
- throw new Error(`${capabilityId}: ${inputError}. Expected input: ${JSON.stringify(capability.inputSchema)}`);
196
- }
190
+ validatePageCapability({ capabilityId, input });
197
191
  switch (capabilityId) {
198
- case 'page.find_elements': {
199
- const role = input['role'];
200
- // The schema's enum check has already run; this narrows the type for the driver.
201
- const filter = {
202
- ...(isAgentElementRole(role) ? { role } : {}),
203
- ...(typeof input['query'] === 'string' ? { query: input['query'] } : {}),
204
- };
205
- const [found, rawPages] = await Promise.all([driver.findElements({}, filter), driver.listPages({})]);
206
- const withState = input['withState'] === true;
207
- const observing = withState && driver.describeState !== undefined;
208
- const elements = await Promise.all(found.map(async (element, index) => {
209
- const label = normalizeAgentLabel({ raw: element.label });
210
- const base = {
211
- handle: element.handle,
212
- role: element.role,
213
- label: label.text,
214
- labelTruncated: label.truncated,
215
- page: element.page,
216
- };
217
- if (!observing || index >= MAX_STATEFUL_ELEMENTS)
218
- return base;
219
- const state = await observeElement(driver, element.handle);
220
- return state === undefined ? base : { ...base, state };
221
- }));
222
- // `label` is page-authored the same way an element's is — bounded here for the same reason
223
- // `page.navigate`'s refusal message bounds it below, rather than trusting a host to have kept
224
- // every `data-agent-page` label short and clean.
225
- const pages = rawPages.map((page) => ({
226
- id: page.id,
227
- label: normalizeAgentLabel({ raw: page.label }).text,
228
- }));
229
- return {
230
- elements,
231
- pages,
232
- untrustedFields: observing
233
- ? ['elements[].label', 'elements[].state.text', 'elements[].state.value', 'pages[].label']
234
- : ['elements[].label', 'pages[].label'],
235
- ...(withState && !observing ? { stateUnavailable: true } : {}),
236
- ...(observing && found.length > MAX_STATEFUL_ELEMENTS ? { stateTruncated: true } : {}),
237
- };
238
- }
192
+ case 'page.find_elements':
193
+ return findPageElements({ driver, input });
239
194
  case 'page.highlight': {
240
195
  const handle = requireHandle(input, 'handle');
241
196
  const durationMs = clampHighlightDuration(input['durationMs']);
@@ -252,30 +207,15 @@ export async function executePageCapability({ driver, capabilityId, input }, _op
252
207
  const observation = await observeWrite(driver, handle, () => driver.click({ handle }));
253
208
  return { clicked: handle, ...observation };
254
209
  }
255
- case 'page.fill': {
256
- const handle = requireHandle(input, 'handle');
257
- // Declared required and typed `string` in the manifest, and `findCapabilityInputError`
258
- // enforced both above — re-checking here would be an unreachable branch, not a safeguard.
259
- const text = input['text'];
260
- // Ask the page what this field actually is before writing to it. A handle proves the page
261
- // published the element; it says nothing about whether it holds a password.
262
- const field = await driver.describeField({ handle });
263
- if (field === null)
264
- throw new Error(`"${handle}" is not a fillable field`);
265
- const refusal = findFieldFillRefusal({ field });
266
- if (refusal !== null) {
267
- throw new Error(`refusing to fill "${handle}": ${describeFieldRefusal({ refusal })}`);
268
- }
269
- const observation = await observeWrite(driver, handle, () => driver.fill({ handle, text }));
270
- return { filled: handle, ...observation };
271
- }
210
+ case 'page.fill':
211
+ return fillPageField({ driver, input });
272
212
  case 'page.select_option': {
273
213
  const handle = requireHandle(input, 'handle');
274
214
  // Required and string-typed by the manifest, already enforced above. See page.fill.
275
215
  const option = input['option'];
276
216
  // Optional and boolean-typed by the manifest; absent means "select it", same as before this
277
217
  // argument existed.
278
- const selected = input['selected'] === false ? false : true;
218
+ const selected = input['selected'] !== false;
279
219
  // No field guard: a dropdown's options are authored by the page, so choosing one reveals
280
220
  // nothing the page had not already published, and none of the credential field types this
281
221
  // surface refuses can be a `<select>`.
@@ -284,37 +224,117 @@ export async function executePageCapability({ driver, capabilityId, input }, _op
284
224
  // "the handle this call acted on" for every verb in this switch (`clicked`, `filled`, …).
285
225
  return { selected: handle, option, optionSelected: selected, ...observation };
286
226
  }
287
- case 'page.navigate': {
288
- // Required and string-typed by the manifest, already enforced above. See page.fill.
289
- const page = input['page'];
290
- const pages = await driver.listPages({});
291
- if (!pages.some((candidate) => candidate.id === page)) {
292
- // Every other piece of page-authored (or, here, caller-authored-but-page-shaped) text
293
- // this system hands to a model goes through normalizeAgentLabel first — bounded, control-
294
- // and bidi-stripped. This refusal was the one path that skipped it: the raw `page`
295
- // argument, and the `pages` this host actually publishes (itself read verbatim off
296
- // `data-agent-page` attributes with no length bound), were interpolated straight into the
297
- // thrown message. A 5000+-char bidi payload survived verbatim as a result. The lookup
298
- // above still runs on the raw values — only the text that reaches the message is bounded.
299
- //
300
- // `id (label)`, not just the id: a refusal is the one moment a caller that guessed wrong
301
- // is looking straight at the real menu, so it is worth spending the label here even though
302
- // `page.find_elements` already carries the same information — a caller that jumped
303
- // straight to `page.navigate` without calling that first still lands on the right id.
304
- const safePage = normalizeAgentLabel({ raw: page }).text;
305
- const safePages = pages.map((candidate) => `${normalizeAgentLabel({ raw: candidate.id }).text} (${normalizeAgentLabel({ raw: candidate.label }).text})`);
306
- throw new Error(`"${safePage}" is not a published page. Available: ${safePages.length > 0 ? safePages.join(', ') : '(none)'}`);
307
- }
308
- const before = await summarizePage(driver);
309
- await driver.navigate({ page });
310
- if (driver.settle !== undefined)
311
- await driver.settle({});
312
- const after = await summarizePage(driver);
313
- return { navigatedTo: page, before, after };
314
- }
227
+ case 'page.navigate':
228
+ return navigatePage({ driver, input });
315
229
  /* c8 ignore next 2 -- unreachable: the id was matched against PAGE_CAPABILITIES above. */
316
230
  default:
317
231
  throw new Error(`unhandled page capability: ${capabilityId}`);
318
232
  }
319
233
  }
234
+ function validatePageCapability({ capabilityId, input }, _optional = {}) {
235
+ const capability = findCapability({ capabilities: PAGE_CAPABILITIES, id: capabilityId });
236
+ if (capability === undefined)
237
+ throw new Error(`unknown page capability: ${capabilityId}`);
238
+ const inputError = findCapabilityInputError({ capability, input });
239
+ if (inputError !== null) {
240
+ // The schema rides along with the refusal rather than making a caller spend a second
241
+ // round trip on describe_tool to learn what it already tried to guess — ai-control-plane.md
242
+ // §29.4's "schema-on-error" requirement, empirically forced by a real run: page.fill was
243
+ // called with `handle` instead of the argument's old name, `element`, twice before the caller
244
+ // gave up and looked the schema up separately. One JSON-Schema object, embedded once, turned
245
+ // that into a same-turn self-correction for every page.* verb this gate guards, not just the
246
+ // one that got measured — self-correction is why that mismatch was survivable at all, given
247
+ // every verb here needs a handle from `page.find_elements`, whose own response has always
248
+ // called this same value `handle` (`AgentElementDescriptor.handle`). The argument itself was
249
+ // later renamed from `element` to `handle` to match, closing the gap this comment describes
250
+ // rather than merely papering over it turn by turn.
251
+ throw new Error(`${capabilityId}: ${inputError}. Expected input: ${JSON.stringify(capability.inputSchema)}`);
252
+ }
253
+ }
254
+ async function findPageElements({ driver, input }, _optional = {}) {
255
+ const role = input['role'];
256
+ // The schema's enum check has already run; this narrows the type for the driver.
257
+ const filter = {
258
+ ...(isAgentElementRole(role) ? { role } : {}),
259
+ ...(typeof input['query'] === 'string' ? { query: input['query'] } : {}),
260
+ };
261
+ const [found, rawPages] = await Promise.all([driver.findElements({}, filter), driver.listPages({})]);
262
+ const withState = input['withState'] === true;
263
+ const observing = withState && driver.describeState !== undefined;
264
+ const elements = await Promise.all(found.map(async (element, index) => {
265
+ const label = normalizeAgentLabel({ raw: element.label });
266
+ const base = {
267
+ handle: element.handle,
268
+ role: element.role,
269
+ label: label.text,
270
+ labelTruncated: label.truncated,
271
+ page: element.page,
272
+ };
273
+ if (!observing || index >= MAX_STATEFUL_ELEMENTS)
274
+ return base;
275
+ const state = await observeElement(driver, element.handle);
276
+ return state === undefined ? base : { ...base, state };
277
+ }));
278
+ // `label` is page-authored the same way an element's is — bounded here for the same reason
279
+ // `page.navigate`'s refusal message bounds it below, rather than trusting a host to have kept
280
+ // every `data-agent-page` label short and clean.
281
+ const pages = rawPages.map((page) => ({
282
+ id: page.id,
283
+ label: normalizeAgentLabel({ raw: page.label }).text,
284
+ }));
285
+ return {
286
+ elements,
287
+ pages,
288
+ untrustedFields: observing
289
+ ? ['elements[].label', 'elements[].state.text', 'elements[].state.value', 'pages[].label']
290
+ : ['elements[].label', 'pages[].label'],
291
+ ...(withState && !observing ? { stateUnavailable: true } : {}),
292
+ ...(observing && found.length > MAX_STATEFUL_ELEMENTS ? { stateTruncated: true } : {}),
293
+ };
294
+ }
295
+ async function fillPageField({ driver, input }, _optional = {}) {
296
+ const handle = requireHandle(input, 'handle');
297
+ // Declared required and typed `string` in the manifest, and `findCapabilityInputError`
298
+ // enforced both above — re-checking here would be an unreachable branch, not a safeguard.
299
+ const text = input['text'];
300
+ // Ask the page what this field actually is before writing to it. A handle proves the page
301
+ // published the element; it says nothing about whether it holds a password.
302
+ const field = await driver.describeField({ handle });
303
+ if (field === null)
304
+ throw new Error(`"${handle}" is not a fillable field`);
305
+ const refusal = findFieldFillRefusal({ field });
306
+ if (refusal !== null) {
307
+ throw new Error(`refusing to fill "${handle}": ${describeFieldRefusal({ refusal })}`);
308
+ }
309
+ const observation = await observeWrite(driver, handle, () => driver.fill({ handle, text }));
310
+ return { filled: handle, ...observation };
311
+ }
312
+ async function navigatePage({ driver, input }, _optional = {}) {
313
+ // Required and string-typed by the manifest, already enforced above. See page.fill.
314
+ const page = input['page'];
315
+ const pages = await driver.listPages({});
316
+ if (!pages.some((candidate) => candidate.id === page)) {
317
+ // Every other piece of page-authored (or, here, caller-authored-but-page-shaped) text
318
+ // this system hands to a model goes through normalizeAgentLabel first — bounded, control-
319
+ // and bidi-stripped. This refusal was the one path that skipped it: the raw `page`
320
+ // argument, and the `pages` this host actually publishes (itself read verbatim off
321
+ // `data-agent-page` attributes with no length bound), were interpolated straight into the
322
+ // thrown message. A 5000+-char bidi payload survived verbatim as a result. The lookup
323
+ // above still runs on the raw values — only the text that reaches the message is bounded.
324
+ //
325
+ // `id (label)`, not just the id: a refusal is the one moment a caller that guessed wrong
326
+ // is looking straight at the real menu, so it is worth spending the label here even though
327
+ // `page.find_elements` already carries the same information — a caller that jumped
328
+ // straight to `page.navigate` without calling that first still lands on the right id.
329
+ const safePage = normalizeAgentLabel({ raw: page }).text;
330
+ const safePages = pages.map((candidate) => `${normalizeAgentLabel({ raw: candidate.id }).text} (${normalizeAgentLabel({ raw: candidate.label }).text})`);
331
+ throw new Error(`"${safePage}" is not a published page. Available: ${safePages.length > 0 ? safePages.join(', ') : '(none)'}`);
332
+ }
333
+ const before = await summarizePage(driver);
334
+ await driver.navigate({ page });
335
+ if (driver.settle !== undefined)
336
+ await driver.settle({});
337
+ const after = await summarizePage(driver);
338
+ return { navigatedTo: page, before, after };
339
+ }
320
340
  //# sourceMappingURL=page-executor.js.map
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './topic-schema.js';
3
+ export * from './site-guide.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './topic-schema.js';
3
+ export * from './site-guide.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ import type { SiteGuide, SiteGuidePorts } from './types.js';
2
+ export declare class SiteGuideInputError extends Error {
3
+ constructor({ message }: {
4
+ readonly message: string;
5
+ }, _options?: Record<string, never>);
6
+ }
7
+ /** Build a neutral read-only guide over host ports. No product state or plugin loader is ambient.
8
+ * Authorization and contributions are read fresh for each request. Errors from enrichment/facts
9
+ * yield explicit unavailable status without copying arbitrary error messages into model context.
10
+ * @param ports - Facts, authorization, optional enrichment, and host minimal capability cards.
11
+ * @returns One overview/explain service. Invalid input and hidden topics throw SiteGuideInputError.
12
+ * @complexity O(t + p) per request, topics plus distinct permissions; one bulk facts read.
13
+ * @example createSiteGuide({ facts, authorize, builtInTopics: cards }).run({ caller, operation: 'overview' });
14
+ */
15
+ export declare function createSiteGuide(ports: SiteGuidePorts, options?: {
16
+ readonly readTimeoutMs?: number;
17
+ }): SiteGuide;
18
+ //# sourceMappingURL=site-guide.d.ts.map