@jini-ai/agentic 0.4.3 → 0.4.4
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/dist/core/handle.d.ts +13 -0
- package/dist/core/handle.js +19 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/site-guide/index.d.ts +4 -0
- package/dist/core/site-guide/index.js +4 -0
- package/dist/core/site-guide/site-guide.d.ts +18 -0
- package/dist/core/site-guide/site-guide.js +144 -0
- package/dist/core/site-guide/topic-schema.d.ts +16 -0
- package/dist/core/site-guide/topic-schema.js +68 -0
- package/dist/core/site-guide/types.d.ts +83 -0
- package/dist/core/site-guide/types.js +2 -0
- package/dist/skills/install/archive.js +1 -0
- package/dist/skills/install/index.d.ts +1 -0
- package/dist/skills/install/index.js +1 -0
- package/dist/skills/install/install-service.d.ts +20 -1
- package/dist/skills/install/install-service.js +23 -11
- package/dist/skills/install/layout.d.ts +8 -0
- package/dist/skills/install/layout.js +26 -1
- package/dist/skills/install/live-registration.js +1 -0
- package/dist/skills/install/validation.d.ts +5 -1
- package/dist/skills/install/validation.js +2 -2
- package/dist/skills/install/yauzl-archive-reader.d.ts +29 -0
- package/dist/skills/install/yauzl-archive-reader.js +31 -0
- package/package.json +4 -4
- package/dist/a2ui/agent-to-renderer.d.ts +0 -613
- package/dist/a2ui/agent-to-renderer.js +0 -191
- package/dist/a2ui/catalog.d.ts +0 -73
- package/dist/a2ui/catalog.js +0 -304
- package/dist/a2ui/common-types.d.ts +0 -401
- package/dist/a2ui/common-types.js +0 -130
- package/dist/a2ui/index.d.ts +0 -16
- package/dist/a2ui/index.js +0 -16
- package/dist/a2ui/interpreter.d.ts +0 -71
- package/dist/a2ui/interpreter.js +0 -337
- package/dist/a2ui/json-pointer.d.ts +0 -48
- package/dist/a2ui/json-pointer.js +0 -174
- package/dist/a2ui/renderer-to-agent.d.ts +0 -342
- package/dist/a2ui/renderer-to-agent.js +0 -111
- package/dist/a2ui/resolve.d.ts +0 -28
- package/dist/a2ui/resolve.js +0 -144
- package/dist/a2ui/tree.d.ts +0 -66
- package/dist/a2ui/tree.js +0 -74
- package/dist/ag-ui.d.ts +0 -82
- package/dist/ag-ui.js +0 -85
- package/dist/agui/encoder.d.ts +0 -50
- package/dist/agui/encoder.js +0 -97
- package/dist/agui/events.d.ts +0 -59
- package/dist/agui/events.js +0 -15
- package/dist/capability.d.ts +0 -81
- package/dist/capability.js +0 -79
- package/dist/dom/dom-page-driver.d.ts +0 -36
- package/dist/dom/dom-page-driver.js +0 -601
- package/dist/dom/index.d.ts +0 -17
- package/dist/dom/index.js +0 -17
- package/dist/dom/model-context.d.ts +0 -69
- package/dist/dom/model-context.js +0 -44
- package/dist/element-handles.d.ts +0 -120
- package/dist/element-handles.js +0 -39
- package/dist/gen-ui/encoder.d.ts +0 -50
- package/dist/gen-ui/encoder.js +0 -97
- package/dist/gen-ui/events.d.ts +0 -59
- package/dist/gen-ui/events.js +0 -15
- package/dist/gen-ui/index.d.ts +0 -18
- package/dist/gen-ui/index.js +0 -17
- package/dist/guards.d.ts +0 -96
- package/dist/guards.js +0 -186
- package/dist/handle.d.ts +0 -41
- package/dist/handle.js +0 -41
- package/dist/mcp-ui-apps.d.ts +0 -223
- package/dist/mcp-ui-apps.js +0 -252
- package/dist/mcp-ui.d.ts +0 -133
- package/dist/mcp-ui.js +0 -134
- package/dist/page-capabilities.d.ts +0 -14
- package/dist/page-capabilities.js +0 -129
- package/dist/page-driver.d.ts +0 -120
- package/dist/page-driver.js +0 -2
- package/dist/page-executor.d.ts +0 -94
- package/dist/page-executor.js +0 -320
- package/dist/webmcp.d.ts +0 -202
- package/dist/webmcp.js +0 -173
package/dist/core/handle.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/core/handle.js
CHANGED
|
@@ -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
|
*
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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
|
package/dist/core/index.js
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const MINIMAL_TOPICS = [{
|
|
2
|
+
id: 'site', title: 'How this site works', summary: 'Learn the available building blocks before planning or building.',
|
|
3
|
+
explanation: 'Read the live facts, then consult the host topics and their tools. Missing or forbidden facts are unknown, not disabled capabilities.',
|
|
4
|
+
permissions: [], pointers: [], workflows: [],
|
|
5
|
+
}];
|
|
6
|
+
export class SiteGuideInputError extends Error {
|
|
7
|
+
constructor({ message }, _options = {}) { super(message); this.name = 'SiteGuideInputError'; }
|
|
8
|
+
}
|
|
9
|
+
/** Bound an injected read so an unavailable extension cannot hang learning. Always clear timers.
|
|
10
|
+
* @complexity O(1) beyond the supplied read; rejection text is kept internal to the shell.
|
|
11
|
+
*/
|
|
12
|
+
async function boundedRead(read, timeoutMs) {
|
|
13
|
+
let timer;
|
|
14
|
+
try {
|
|
15
|
+
return await Promise.race([Promise.resolve().then(read), new Promise((_resolve, reject) => {
|
|
16
|
+
timer = setTimeout(() => reject(new Error('site guide read timed out')), timeoutMs);
|
|
17
|
+
})]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timer !== undefined)
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Compose by topic id. Core cards keep their owner; plugins add distinct, permission-gated topics.
|
|
25
|
+
* @complexity O(t) in bounded built-in and contributed topic counts, O(t) lookup space.
|
|
26
|
+
*/
|
|
27
|
+
function combineTopics(builtIns, contributions) {
|
|
28
|
+
const topics = new Map(builtIns.map(topic => [topic.id, { topic, source: 'core' }]));
|
|
29
|
+
const byId = new Map();
|
|
30
|
+
for (const contribution of contributions) {
|
|
31
|
+
for (const topic of contribution.topics) {
|
|
32
|
+
const candidates = byId.get(topic.id) ?? [];
|
|
33
|
+
candidates.push({ topic, pluginId: contribution.pluginId });
|
|
34
|
+
byId.set(topic.id, candidates);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let conflicts = false;
|
|
38
|
+
for (const [id, candidates] of byId) {
|
|
39
|
+
// A contribution cannot replace a host's prose, pointers, workflows or security gates.
|
|
40
|
+
if (topics.has(id) || candidates.length !== 1) {
|
|
41
|
+
conflicts = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const candidate = candidates[0];
|
|
45
|
+
topics.set(id, { topic: candidate.topic, source: candidate.pluginId });
|
|
46
|
+
}
|
|
47
|
+
return { topics: [...topics.values()], conflicts };
|
|
48
|
+
}
|
|
49
|
+
/** Build a neutral read-only guide over host ports. No product state or plugin loader is ambient.
|
|
50
|
+
* Authorization and contributions are read fresh for each request. Errors from enrichment/facts
|
|
51
|
+
* yield explicit unavailable status without copying arbitrary error messages into model context.
|
|
52
|
+
* @param ports - Facts, authorization, optional enrichment, and host minimal capability cards.
|
|
53
|
+
* @returns One overview/explain service. Invalid input and hidden topics throw SiteGuideInputError.
|
|
54
|
+
* @complexity O(t + p) per request, topics plus distinct permissions; one bulk facts read.
|
|
55
|
+
* @example createSiteGuide({ facts, authorize, builtInTopics: cards }).run({ caller, operation: 'overview' });
|
|
56
|
+
*/
|
|
57
|
+
export function createSiteGuide(ports, options = {}) {
|
|
58
|
+
const timeoutMs = options.readTimeoutMs ?? 5_000;
|
|
59
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
60
|
+
throw new SiteGuideInputError({ message: 'readTimeoutMs must be a positive finite number' });
|
|
61
|
+
const builtIns = ports.builtInTopics ?? MINIMAL_TOPICS;
|
|
62
|
+
if (builtIns.length > 256 || new Set(builtIns.map(topic => topic.id)).size !== builtIns.length)
|
|
63
|
+
throw new SiteGuideInputError({ message: 'Built-in topics must have unique ids and contain at most 256 cards' });
|
|
64
|
+
return { async run(input, _options = {}) {
|
|
65
|
+
if (input.operation !== 'overview' && input.operation !== 'explain')
|
|
66
|
+
throw new SiteGuideInputError({ message: 'operation must be overview or explain' });
|
|
67
|
+
if (input.operation === 'overview' && input.topic !== undefined)
|
|
68
|
+
throw new SiteGuideInputError({ message: 'topic is only accepted with operation explain' });
|
|
69
|
+
if (input.operation === 'explain' && (typeof input.topic !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(input.topic))) {
|
|
70
|
+
throw new SiteGuideInputError({ message: 'explain requires a topic id from overview' });
|
|
71
|
+
}
|
|
72
|
+
let contributions = [];
|
|
73
|
+
let enrichment = 'ok';
|
|
74
|
+
if (ports.loadContributions) {
|
|
75
|
+
try {
|
|
76
|
+
const loaded = await boundedRead(() => ports.loadContributions({ caller: input.caller }), timeoutMs);
|
|
77
|
+
if (loaded.items.reduce((count, item) => count + item.topics.length, 0) > 256)
|
|
78
|
+
throw new SiteGuideInputError({ message: 'Too many contributed topics' });
|
|
79
|
+
contributions = loaded.items;
|
|
80
|
+
if (loaded.refusals.length)
|
|
81
|
+
enrichment = 'partial';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
enrichment = 'unavailable';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const combined = combineTopics(builtIns, contributions);
|
|
88
|
+
if (combined.conflicts)
|
|
89
|
+
enrichment = 'partial';
|
|
90
|
+
const decisions = new Map();
|
|
91
|
+
const allowed = (permission) => {
|
|
92
|
+
let decision = decisions.get(permission);
|
|
93
|
+
if (!decision) {
|
|
94
|
+
// Failed authorization is denial, never an excuse to expose a card or run its read.
|
|
95
|
+
decision = boundedRead(() => ports.authorize({ caller: input.caller, permission }), timeoutMs).catch(() => false);
|
|
96
|
+
decisions.set(permission, decision);
|
|
97
|
+
}
|
|
98
|
+
return decision;
|
|
99
|
+
};
|
|
100
|
+
const permitted = async (permissions) => (await Promise.all(permissions.map(allowed))).every(Boolean);
|
|
101
|
+
const views = [];
|
|
102
|
+
for (const entry of combined.topics) {
|
|
103
|
+
if (input.operation === 'explain' && entry.topic.id !== input.topic)
|
|
104
|
+
continue;
|
|
105
|
+
if (!await permitted(entry.topic.permissions))
|
|
106
|
+
continue;
|
|
107
|
+
const view = { id: entry.topic.id, title: entry.topic.title, summary: entry.topic.summary, source: entry.source };
|
|
108
|
+
if (input.operation === 'explain') {
|
|
109
|
+
const pointers = [];
|
|
110
|
+
for (const { permissions, ...pointer } of entry.topic.pointers)
|
|
111
|
+
if (await permitted(permissions))
|
|
112
|
+
pointers.push(pointer);
|
|
113
|
+
const workflows = [];
|
|
114
|
+
for (const { permissions, ...workflow } of entry.topic.workflows)
|
|
115
|
+
if (await permitted(permissions))
|
|
116
|
+
workflows.push(workflow);
|
|
117
|
+
views.push({ ...view, explanation: entry.topic.explanation, pointers, workflows });
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
views.push(view);
|
|
121
|
+
}
|
|
122
|
+
if (input.operation === 'explain' && views.length === 0)
|
|
123
|
+
throw new SiteGuideInputError({ message: 'Topic unavailable. Call site_guide with operation overview for topics you may read.' });
|
|
124
|
+
let facts = {};
|
|
125
|
+
const topicIds = views.map(view => view.id);
|
|
126
|
+
// Overview is discovery only; read fresh domain facts when a visible topic is explained.
|
|
127
|
+
if (input.operation === 'explain') {
|
|
128
|
+
try {
|
|
129
|
+
const read = await boundedRead(() => ports.facts.read({ caller: input.caller, topics: topicIds }), timeoutMs);
|
|
130
|
+
// A facts adapter returning an unrelated section cannot disclose it through this tool.
|
|
131
|
+
facts = Object.fromEntries(topicIds.map(id => [id, read[id] ?? { status: 'unavailable' }]));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
facts = Object.fromEntries(topicIds.map(id => [id, { status: 'unavailable' }]));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const callerPermissions = [];
|
|
138
|
+
for (const [permission, decision] of decisions)
|
|
139
|
+
if (await decision)
|
|
140
|
+
callerPermissions.push(permission);
|
|
141
|
+
return { operation: input.operation, topics: views, facts, callerPermissions, enrichment };
|
|
142
|
+
} };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=site-guide.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GuideTopic } from './types.js';
|
|
2
|
+
/** Parse bounded data-only enrichment. Invalid fields and duplicate ids refuse the whole file.
|
|
3
|
+
* @returns Validated topics or a fixed, data-free diagnostic; never quotes plugin bytes.
|
|
4
|
+
* @complexity O(n) characters, capped at 256 KiB of UTF-8, plus bounded shape traversal.
|
|
5
|
+
* @example parseGuideTopics({ raw: '{"version":1,"topics":[]}' });
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseGuideTopics({ raw }: {
|
|
8
|
+
readonly raw: string;
|
|
9
|
+
}, _options?: Record<string, never>): {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly descriptors: readonly GuideTopic[];
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=topic-schema.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Native shape guards keep the core barrel usable without the optional A2UI/Zod peer. */
|
|
2
|
+
function record(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function text(value, maximum = 8_000) {
|
|
6
|
+
return typeof value === 'string' && value.length > 0 && value.length <= maximum;
|
|
7
|
+
}
|
|
8
|
+
function keys(value, permitted) {
|
|
9
|
+
return Object.keys(value).every(key => permitted.includes(key));
|
|
10
|
+
}
|
|
11
|
+
function list(value, maximum, guard) {
|
|
12
|
+
return Array.isArray(value) && value.length <= maximum && value.every(guard);
|
|
13
|
+
}
|
|
14
|
+
function permission(value) { return text(value, 128); }
|
|
15
|
+
function permissions(value) { return list(value, 32, permission); }
|
|
16
|
+
function step(value) { return text(value); }
|
|
17
|
+
/** Accept document/admin/HTTPS pointers, never script URLs or arbitrary local files. */
|
|
18
|
+
function href(value) {
|
|
19
|
+
return text(value, 512) && (value.startsWith('/admin/') || value.startsWith('development/') || value.startsWith('https://'));
|
|
20
|
+
}
|
|
21
|
+
function pointer(value) {
|
|
22
|
+
if (!record(value) || !keys(value, ['label', 'tool', 'href', 'permissions']))
|
|
23
|
+
return false;
|
|
24
|
+
if (!text(value.label) || !permissions(value.permissions))
|
|
25
|
+
return false;
|
|
26
|
+
if (value.tool !== undefined && !text(value.tool, 128))
|
|
27
|
+
return false;
|
|
28
|
+
if (value.href !== undefined && !href(value.href))
|
|
29
|
+
return false;
|
|
30
|
+
return value.tool !== undefined || value.href !== undefined;
|
|
31
|
+
}
|
|
32
|
+
function workflow(value) {
|
|
33
|
+
if (!record(value) || !keys(value, ['title', 'steps', 'permissions']))
|
|
34
|
+
return false;
|
|
35
|
+
return text(value.title) && permissions(value.permissions) && list(value.steps, 16, step) && value.steps.length > 0;
|
|
36
|
+
}
|
|
37
|
+
function topic(value) {
|
|
38
|
+
if (!record(value) || !keys(value, ['id', 'title', 'summary', 'explanation', 'permissions', 'pointers', 'workflows']))
|
|
39
|
+
return false;
|
|
40
|
+
if (!text(value.id, 64) || !/^[a-z][a-z0-9-]*$/.test(value.id))
|
|
41
|
+
return false;
|
|
42
|
+
return text(value.title, 128) && text(value.summary, 1_000) && text(value.explanation) && permissions(value.permissions)
|
|
43
|
+
&& list(value.pointers, 24, pointer) && list(value.workflows, 16, workflow);
|
|
44
|
+
}
|
|
45
|
+
/** Parse bounded data-only enrichment. Invalid fields and duplicate ids refuse the whole file.
|
|
46
|
+
* @returns Validated topics or a fixed, data-free diagnostic; never quotes plugin bytes.
|
|
47
|
+
* @complexity O(n) characters, capped at 256 KiB of UTF-8, plus bounded shape traversal.
|
|
48
|
+
* @example parseGuideTopics({ raw: '{"version":1,"topics":[]}' });
|
|
49
|
+
*/
|
|
50
|
+
export function parseGuideTopics({ raw }, _options = {}) {
|
|
51
|
+
if (raw.length > 262_144 || new TextEncoder().encode(raw).byteLength > 262_144)
|
|
52
|
+
return { ok: false, reason: 'site-guide manifest exceeds 256 KiB' };
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
value = JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { ok: false, reason: 'site-guide manifest must be valid JSON' };
|
|
59
|
+
}
|
|
60
|
+
if (!record(value) || !keys(value, ['version', 'topics']) || value.version !== 1 || !list(value.topics, 64, topic)) {
|
|
61
|
+
return { ok: false, reason: 'site-guide manifest does not match version 1 topic schema' };
|
|
62
|
+
}
|
|
63
|
+
const ids = new Set(value.topics.map(item => item.id));
|
|
64
|
+
if (ids.size !== value.topics.length)
|
|
65
|
+
return { ok: false, reason: 'site-guide manifest contains duplicate topic ids' };
|
|
66
|
+
return { ok: true, descriptors: value.topics };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=topic-schema.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Read-only learning contracts. Hosts supply facts; plugins supply prose, never read effects. */
|
|
2
|
+
export interface GuideCaller {
|
|
3
|
+
readonly principalId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface GuidePointer {
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly tool?: string | undefined;
|
|
8
|
+
readonly href?: string | undefined;
|
|
9
|
+
readonly permissions: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface GuideWorkflow {
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly steps: readonly string[];
|
|
14
|
+
readonly permissions: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
export interface GuideTopic {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly summary: string;
|
|
20
|
+
readonly explanation: string;
|
|
21
|
+
readonly permissions: readonly string[];
|
|
22
|
+
readonly pointers: readonly GuidePointer[];
|
|
23
|
+
readonly workflows: readonly GuideWorkflow[];
|
|
24
|
+
}
|
|
25
|
+
export interface GuideContribution {
|
|
26
|
+
readonly pluginId: string;
|
|
27
|
+
readonly topics: readonly GuideTopic[];
|
|
28
|
+
}
|
|
29
|
+
export interface GuideContributionResult {
|
|
30
|
+
readonly items: readonly GuideContribution[];
|
|
31
|
+
/** Host-local diagnostics; never expose arbitrary loader messages to the model. */
|
|
32
|
+
readonly refusals: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export type GuideFact = {
|
|
35
|
+
readonly status: 'ok';
|
|
36
|
+
readonly data: unknown;
|
|
37
|
+
readonly truncated?: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
readonly status: 'forbidden' | 'unavailable';
|
|
40
|
+
};
|
|
41
|
+
/** Only a permission-scoped, explicitly projected DTO may leave the host adapter. */
|
|
42
|
+
export interface SiteFactsPort {
|
|
43
|
+
read(input: {
|
|
44
|
+
readonly caller: GuideCaller;
|
|
45
|
+
readonly topics: readonly string[];
|
|
46
|
+
}, options?: Record<string, never>): Promise<Readonly<Record<string, GuideFact>>>;
|
|
47
|
+
}
|
|
48
|
+
export interface SiteGuidePorts {
|
|
49
|
+
readonly facts: SiteFactsPort;
|
|
50
|
+
readonly authorize: (input: {
|
|
51
|
+
readonly caller: GuideCaller;
|
|
52
|
+
readonly permission: string;
|
|
53
|
+
}, options?: Record<string, never>) => Promise<boolean>;
|
|
54
|
+
readonly loadContributions?: (input: {
|
|
55
|
+
readonly caller: GuideCaller;
|
|
56
|
+
}, options?: Record<string, never>) => Promise<GuideContributionResult>;
|
|
57
|
+
readonly builtInTopics?: readonly GuideTopic[];
|
|
58
|
+
}
|
|
59
|
+
export interface GuideTopicView {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly summary: string;
|
|
63
|
+
readonly explanation?: string;
|
|
64
|
+
readonly pointers?: readonly Omit<GuidePointer, 'permissions'>[];
|
|
65
|
+
readonly workflows?: readonly Omit<GuideWorkflow, 'permissions'>[];
|
|
66
|
+
readonly source: string;
|
|
67
|
+
}
|
|
68
|
+
export interface GuideResult {
|
|
69
|
+
readonly operation: 'overview' | 'explain';
|
|
70
|
+
readonly topics: readonly GuideTopicView[];
|
|
71
|
+
readonly facts: Readonly<Record<string, GuideFact>>;
|
|
72
|
+
/** Granted permissions consulted for this result, rather than an inventory of the caller's roles. */
|
|
73
|
+
readonly callerPermissions: readonly string[];
|
|
74
|
+
readonly enrichment: 'ok' | 'unavailable' | 'partial';
|
|
75
|
+
}
|
|
76
|
+
export interface SiteGuide {
|
|
77
|
+
run(input: {
|
|
78
|
+
readonly caller: GuideCaller;
|
|
79
|
+
readonly operation: 'overview' | 'explain';
|
|
80
|
+
readonly topic?: string;
|
|
81
|
+
}, options?: Record<string, never>): Promise<GuideResult>;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -37,6 +37,7 @@ export async function readSkillArchive({ base64, archiveReader }, _optional = {}
|
|
|
37
37
|
finally {
|
|
38
38
|
reader.close({});
|
|
39
39
|
}
|
|
40
|
+
/** Converts malformed archive parser failures into a client-actionable validation refusal. */
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
42
43
|
if (error instanceof SkillInputError)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SkillUploadFile } from "./validation.js";
|
|
1
|
+
import { validateSkillFiles, type SkillUploadFile } from "./validation.js";
|
|
2
2
|
import { type GitHubSkillSource } from "./github.js";
|
|
3
3
|
import type { SkillInstallDeps } from "./ports.js";
|
|
4
4
|
export interface ManagedSkill {
|
|
@@ -17,7 +17,26 @@ export type SkillInstallInput = {
|
|
|
17
17
|
} | {
|
|
18
18
|
readonly archiveBase64: string;
|
|
19
19
|
});
|
|
20
|
+
/** A fetched and fully validated skill package, not yet written. Holding the validated bytes (not
|
|
21
|
+
* the source) means commitSkillInstall writes exactly what was validated, with no second fetch. */
|
|
22
|
+
export interface PreparedSkillInstall {
|
|
23
|
+
readonly toolId: string;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly source: ManagedSkill["source"];
|
|
27
|
+
readonly files: ReturnType<typeof validateSkillFiles>["files"];
|
|
28
|
+
}
|
|
29
|
+
/** Fetch/decode and validate before asking for mutation approval. No filesystem writes. */
|
|
30
|
+
export declare function prepareSkillInstall(required: {
|
|
31
|
+
input: SkillInstallInput;
|
|
32
|
+
deps: Pick<SkillInstallDeps, "fetch" | "archiveReader" | "yamlReader" | "toolId">;
|
|
33
|
+
}, _optional?: {}): Promise<PreparedSkillInstall>;
|
|
20
34
|
export interface SkillInstaller {
|
|
35
|
+
prepareSkillInstall(required: SkillInstallInput): Promise<PreparedSkillInstall>;
|
|
36
|
+
commitSkillInstall(required: {
|
|
37
|
+
workspaceId: string;
|
|
38
|
+
prepared: PreparedSkillInstall;
|
|
39
|
+
}): Promise<ManagedSkill>;
|
|
21
40
|
installSkill(required: SkillInstallInput): Promise<ManagedSkill>;
|
|
22
41
|
listManagedSkills(required: {
|
|
23
42
|
workspaceId: string;
|
|
@@ -3,6 +3,20 @@ import { SkillInputError, validateSkillFiles } from "./validation.js";
|
|
|
3
3
|
import { readSkillArchive } from "./archive.js";
|
|
4
4
|
import { fetchGitHubSkill } from "./github.js";
|
|
5
5
|
import { readSkillState } from "./state.js";
|
|
6
|
+
/** Fetch/decode and validate before asking for mutation approval. No filesystem writes. */
|
|
7
|
+
export async function prepareSkillInstall(required, _optional = {}) {
|
|
8
|
+
const { input, deps } = required;
|
|
9
|
+
let upload;
|
|
10
|
+
let source = "uploaded";
|
|
11
|
+
if ("githubUrl" in input)
|
|
12
|
+
({ files: upload, source } = await fetchGitHubSkill({ githubUrl: input.githubUrl, fetch: deps.fetch }));
|
|
13
|
+
else if ("archiveBase64" in input)
|
|
14
|
+
upload = await readSkillArchive({ base64: input.archiveBase64, archiveReader: deps.archiveReader });
|
|
15
|
+
else
|
|
16
|
+
upload = input.files;
|
|
17
|
+
const validated = validateSkillFiles({ files: upload, yamlReader: deps.yamlReader });
|
|
18
|
+
return { ...validated, toolId: deps.toolId({ name: validated.name }), source };
|
|
19
|
+
}
|
|
6
20
|
/** Validate before writes; publish via rename, with instance-local queues keyed by resolved root.
|
|
7
21
|
* Cross-process exclusivity remains the host filesystem/layout adapter's responsibility.
|
|
8
22
|
*/
|
|
@@ -16,6 +30,7 @@ export function createSkillInstaller(deps, optional = {}) {
|
|
|
16
30
|
throw new SkillInputError({ message: "Invalid skill state file name." });
|
|
17
31
|
return layout;
|
|
18
32
|
};
|
|
33
|
+
/** Serializes mutations in this server without leaving persistent locks after a crash. */
|
|
19
34
|
async function locked(workspaceId, work) {
|
|
20
35
|
const key = layoutFor(workspaceId).workspaceRoot;
|
|
21
36
|
const result = (queues.get(key) ?? Promise.resolve()).catch(() => { }).then(work);
|
|
@@ -47,19 +62,15 @@ export function createSkillInstaller(deps, optional = {}) {
|
|
|
47
62
|
return directory;
|
|
48
63
|
}
|
|
49
64
|
const changed = async (workspaceId, toolId) => { await optional.onChanged?.({ workspaceId, toolId }); };
|
|
50
|
-
|
|
65
|
+
const installer = {
|
|
51
66
|
listManagedSkills,
|
|
67
|
+
prepareSkillInstall: input => prepareSkillInstall({ input, deps }),
|
|
52
68
|
async installSkill(input) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
upload = await readSkillArchive({ base64: input.archiveBase64, archiveReader: deps.archiveReader });
|
|
59
|
-
else
|
|
60
|
-
upload = input.files;
|
|
61
|
-
const validated = validateSkillFiles({ files: upload, yamlReader: deps.yamlReader });
|
|
62
|
-
const toolId = deps.toolId({ name: validated.name });
|
|
69
|
+
return installer.commitSkillInstall({ workspaceId: input.workspaceId, prepared: await prepareSkillInstall({ input, deps }) });
|
|
70
|
+
},
|
|
71
|
+
async commitSkillInstall(input) {
|
|
72
|
+
const validated = input.prepared;
|
|
73
|
+
const { toolId, source } = validated;
|
|
63
74
|
const layout = layoutFor(input.workspaceId);
|
|
64
75
|
return locked(input.workspaceId, async () => {
|
|
65
76
|
if ((await listManagedSkills(input)).some(skill => skill.toolId === toolId))
|
|
@@ -112,6 +123,7 @@ export function createSkillInstaller(deps, optional = {}) {
|
|
|
112
123
|
});
|
|
113
124
|
},
|
|
114
125
|
};
|
|
126
|
+
return installer;
|
|
115
127
|
}
|
|
116
128
|
function containedDirectory(root, directory) {
|
|
117
129
|
if (!directory || directory === "." || directory === ".." || /[/\\\x00]/.test(directory))
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { SkillLayoutPort } from './ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* Deliberately does NOT import anything from `src/features/agent-plugins/` — Agent Skills and Agent
|
|
4
|
+
* Plugins are different things with their own directories (owner decision, above), and this file's
|
|
5
|
+
* small id-segment guard is duplicated rather than shared, for the same reason
|
|
6
|
+
* `capability-projection.ts`/`capability-source.ts` already duplicate their own `humanize` helper
|
|
7
|
+
* between themselves: a private guard belonging to one small module, not a promise to keep two
|
|
8
|
+
* independent features' internals in sync.
|
|
9
|
+
*/
|
|
2
10
|
/** Every storage location is host-owned; this adapter has no product or cwd defaults.
|
|
3
11
|
* Layout resolves the same required root/stagingRoot/workspaceDirectory/stateFileName
|
|
4
12
|
* contract used by the installer. No site, environment or cwd defaults are allowed: separate
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* One safe, indivisible path segment: lowercase alphanumerics in `-`/`.`-separated runs, with no
|
|
4
|
+
* leading, trailing, or doubled separator. Mirrors `agent-plugins/layout.ts`'s own
|
|
5
|
+
* `SAFE_ID_SEGMENT_PATTERN` — duplicated rather than imported (see this file's header) — and accepts
|
|
6
|
+
* this instance's real workspace id (`workspace-local`, per `<site>/uploads/ws/workspace-local/`) as
|
|
7
|
+
* well as every UUID.
|
|
8
|
+
*/
|
|
9
|
+
const SAFE_ID_SEGMENT_PATTERN = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
|
|
10
|
+
/** Bounds the segment so a caller cannot drive a pathological path length. */
|
|
11
|
+
const MAX_ID_SEGMENT_LENGTH = 64;
|
|
12
|
+
// The segment grammar above moved from Tovu features/skills/layout.ts; the sibling path in its
|
|
13
|
+
// historical rationale refers to Tovu features/agent-plugins/layout.ts.
|
|
14
|
+
/**
|
|
15
|
+
* Deliberately does NOT import anything from `src/features/agent-plugins/` — Agent Skills and Agent
|
|
16
|
+
* Plugins are different things with their own directories (owner decision, above), and this file's
|
|
17
|
+
* small id-segment guard is duplicated rather than shared, for the same reason
|
|
18
|
+
* `capability-projection.ts`/`capability-source.ts` already duplicate their own `humanize` helper
|
|
19
|
+
* between themselves: a private guard belonging to one small module, not a promise to keep two
|
|
20
|
+
* independent features' internals in sync.
|
|
21
|
+
*/
|
|
22
|
+
// This paragraph records the original Tovu module boundary; the private segment guard now
|
|
23
|
+
// belongs to this Jini layout owner, and the host keeps only the site-directory adapter.
|
|
2
24
|
/** Every storage location is host-owned; this adapter has no product or cwd defaults.
|
|
3
25
|
* Layout resolves the same required root/stagingRoot/workspaceDirectory/stateFileName
|
|
4
26
|
* contract used by the installer. No site, environment or cwd defaults are allowed: separate
|
|
@@ -13,8 +35,11 @@ export function createSkillLayout(required, _optional = {}) {
|
|
|
13
35
|
if (!stateFileName || /[/\\\x00]/.test(stateFileName) || ['.', '..', 'SKILL.md', 'README.md', 'LICENSE', 'references', 'scripts', 'assets'].includes(stateFileName))
|
|
14
36
|
throw new Error('Invalid skill state file name.');
|
|
15
37
|
return Object.freeze({ resolve({ workspaceId }) {
|
|
38
|
+
// Normalize BEFORE validating, not after — see `agent-plugins/layout.ts`'s own comment at the
|
|
39
|
+
// identical line: the pattern is deliberately lowercase-only, and the normalized value is the
|
|
40
|
+
// one that actually becomes the path segment, so it is the only value worth asserting about.
|
|
16
41
|
const normalized = workspaceId.toLowerCase();
|
|
17
|
-
if (normalized.length >
|
|
42
|
+
if (normalized.length > MAX_ID_SEGMENT_LENGTH || !SAFE_ID_SEGMENT_PATTERN.test(normalized))
|
|
18
43
|
throw new Error(`resolve: '${workspaceId}' is not a valid workspace id`);
|
|
19
44
|
return { workspaceRoot: path.join(root, workspaceDirectory, normalized), stagingRoot: path.resolve(stagingRoot), stateFileName };
|
|
20
45
|
} });
|
|
@@ -8,6 +8,7 @@ export function createLiveSkillRegistration(required, _optional = {}) {
|
|
|
8
8
|
registry.has = input => slots.has(input.id) ? active.has(input.id) : has(input);
|
|
9
9
|
let fingerprint = '';
|
|
10
10
|
return ({ tools }) => {
|
|
11
|
+
// Discovery changes invalidate the search snapshot; handlers always use current guidance.
|
|
11
12
|
const nextFingerprint = JSON.stringify(tools.map(t => t.descriptor));
|
|
12
13
|
const next = new Map(tools.map(t => [t.descriptor.id, t]));
|
|
13
14
|
if (next.size !== tools.length)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { YamlReaderPort } from "./ports.js";
|
|
2
2
|
export type SkillYamlPort = YamlReaderPort;
|
|
3
3
|
export declare class SkillInputError extends Error {
|
|
4
|
-
constructor(
|
|
4
|
+
constructor(required: {
|
|
5
5
|
message: string;
|
|
6
6
|
}, optional?: {
|
|
7
7
|
cause?: unknown;
|
|
8
8
|
});
|
|
9
|
+
/** Published host adapters may still use the native Error call shape during migration. */
|
|
10
|
+
constructor(message: string, optional?: {
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
});
|
|
9
13
|
}
|
|
10
14
|
export interface SkillUploadFile {
|
|
11
15
|
readonly path: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Bounded skill-bundle validation; scripts are stored as data and never executed. */
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export class SkillInputError extends Error {
|
|
4
|
-
constructor(
|
|
5
|
-
super(message, optional);
|
|
4
|
+
constructor(required, optional = {}) {
|
|
5
|
+
super(typeof required === "string" ? required : required.message, optional);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
export const MAX_SKILL_BYTES = 8 * 1024 * 1024;
|