appilot 0.0.1 → 0.1.1
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/README.md +101 -7
- package/dist/dom/sensitiveFields.d.ts +33 -0
- package/dist/dom/sensitiveFields.js +72 -0
- package/dist/dom/surfaces.d.ts +81 -0
- package/dist/dom/surfaces.js +91 -0
- package/dist/dom/uniqueSelector.d.ts +23 -0
- package/dist/dom/uniqueSelector.js +104 -0
- package/dist/domResponder.d.ts +38 -0
- package/dist/domResponder.js +355 -0
- package/dist/focusedSessions/pageSdk.d.ts +107 -0
- package/dist/focusedSessions/pageSdk.js +238 -0
- package/dist/httpFetch.d.ts +52 -0
- package/dist/httpFetch.js +137 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +24 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +21 -0
- package/dist/sessions/types.d.ts +140 -0
- package/dist/sessions/types.js +12 -0
- package/dist/webmcp/registry.d.ts +75 -0
- package/dist/webmcp/registry.js +152 -0
- package/dist/webmcp/sdk.d.ts +38 -0
- package/dist/webmcp/sdk.js +81 -0
- package/dist/widget/boot.d.ts +91 -0
- package/dist/widget/boot.js +313 -0
- package/package.json +51 -16
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused Sessions page SDK: the surface a host app calls to run an
|
|
3
|
+
* app-initiated, mission-scoped agent conversation and receive its structured
|
|
4
|
+
* outcome (docs/agent/focused-sessions.md).
|
|
5
|
+
*
|
|
6
|
+
* const handle = await startFocusedSession({ template_id: 'role-play', variables: { role: 'Sam' } });
|
|
7
|
+
* handle.onOutcome(outcome => saveEvidence(outcome));
|
|
8
|
+
*
|
|
9
|
+
* Architecture mirrors the WebMCP registry: the SDK itself performs NO HTTP
|
|
10
|
+
* and holds NO credentials. It delegates to a PROVIDER the assistant surface
|
|
11
|
+
* registers on `window.__APPILOT_SESSIONS__`:
|
|
12
|
+
* - the widget registers a direct provider (it shares the page realm and
|
|
13
|
+
* owns the widget-key + user-token REST calls), and
|
|
14
|
+
* - the extension's MAIN-world script registers a postMessage relay to its
|
|
15
|
+
* isolated content script -> background -> JWT REST.
|
|
16
|
+
* A window-anchored global (not a module singleton) because the host app's
|
|
17
|
+
* bundle and the assistant surface's bundle are DIFFERENT module graphs; a
|
|
18
|
+
* module-level variable would give each its own copy.
|
|
19
|
+
*
|
|
20
|
+
* Import from `appilot` (same shipping posture as `registerTool`).
|
|
21
|
+
*/
|
|
22
|
+
const GLOBAL_KEY = '__APPILOT_SESSIONS__';
|
|
23
|
+
const PROVIDER_WAIT_TIMEOUT_MS = 10_000;
|
|
24
|
+
function getWindow() {
|
|
25
|
+
return typeof window !== 'undefined' ? window : undefined;
|
|
26
|
+
}
|
|
27
|
+
function getGlobal() {
|
|
28
|
+
const w = getWindow();
|
|
29
|
+
if (!w)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!w[GLOBAL_KEY])
|
|
32
|
+
w[GLOBAL_KEY] = {};
|
|
33
|
+
return w[GLOBAL_KEY];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Called by the assistant surface (widget boot / extension MAIN-world bridge)
|
|
37
|
+
* to expose the session transport to the page. Last registration wins: when
|
|
38
|
+
* both the widget and the extension are present on a page, the most recent
|
|
39
|
+
* surface serves new sessions (in practice hosts embed exactly one).
|
|
40
|
+
*/
|
|
41
|
+
export function registerFocusedSessionProvider(provider) {
|
|
42
|
+
const g = getGlobal();
|
|
43
|
+
if (!g)
|
|
44
|
+
return;
|
|
45
|
+
g.provider = provider;
|
|
46
|
+
const waiters = g.waiters ?? [];
|
|
47
|
+
g.waiters = [];
|
|
48
|
+
for (const w of waiters) {
|
|
49
|
+
try {
|
|
50
|
+
w(provider);
|
|
51
|
+
}
|
|
52
|
+
catch { /* waiter errors are the page's problem */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function waitForProvider(timeoutMs) {
|
|
56
|
+
const g = getGlobal();
|
|
57
|
+
if (!g)
|
|
58
|
+
return Promise.reject(new Error('Focused sessions require a browser window'));
|
|
59
|
+
if (g.provider)
|
|
60
|
+
return Promise.resolve(g.provider);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
const idx = g.waiters?.indexOf(onReady) ?? -1;
|
|
64
|
+
if (idx >= 0)
|
|
65
|
+
g.waiters?.splice(idx, 1);
|
|
66
|
+
reject(new Error('No Appilot assistant surface available (widget not loaded / extension not active)'));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
const onReady = (provider) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
resolve(provider);
|
|
71
|
+
};
|
|
72
|
+
g.waiters = g.waiters ?? [];
|
|
73
|
+
g.waiters.push(onReady);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start a focused session. Resolves once the assistant surface has created
|
|
78
|
+
* the session server-side (mission resolved + conversation bound); rejects
|
|
79
|
+
* with the surface's typed error message when the template is unknown, a
|
|
80
|
+
* required variable is missing, or no assistant surface is present.
|
|
81
|
+
*/
|
|
82
|
+
export async function startFocusedSession(request) {
|
|
83
|
+
// Wake the assistant surface BEFORE waiting for the provider. The widget
|
|
84
|
+
// registers its provider from the lazily-loaded panel chunk, so on a page
|
|
85
|
+
// where the user never opened the panel there is NO provider yet; without
|
|
86
|
+
// this signal, start would deadlock into the wait timeout (found in the
|
|
87
|
+
// first L3 Learn headful soak). The widget loader listens for
|
|
88
|
+
// `appilot:open` (the same event behind `window.Appilot.open()`) and
|
|
89
|
+
// mounts the panel, which binds the provider and resolves the waiter.
|
|
90
|
+
// Opening the panel is also the DESIRED UX: the session dialogue happens
|
|
91
|
+
// there. Surfaces that register eagerly (extension MAIN-world bridge)
|
|
92
|
+
// simply never hear the event.
|
|
93
|
+
try {
|
|
94
|
+
getWindow()?.dispatchEvent(new CustomEvent('appilot:open'));
|
|
95
|
+
}
|
|
96
|
+
catch { /* older browsers without CustomEvent; the eager-surface path still works */ }
|
|
97
|
+
const provider = await waitForProvider(PROVIDER_WAIT_TIMEOUT_MS);
|
|
98
|
+
const started = await provider.start(request);
|
|
99
|
+
storeSessionId(started.focused_session_id);
|
|
100
|
+
return buildHandle(provider, started);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The page-side handle. Shared by `startFocusedSession` and
|
|
104
|
+
* `resumeFocusedSession` so a resumed session behaves identically to a fresh
|
|
105
|
+
* one, terminal latch included.
|
|
106
|
+
*/
|
|
107
|
+
function buildHandle(provider, session) {
|
|
108
|
+
// Handle-level terminal latch, defence in depth on top of the provider's
|
|
109
|
+
// per-session exactly-once guarantee: the FIRST terminal event (outcome OR
|
|
110
|
+
// ended) settles the handle, later events of the other kind are ignored,
|
|
111
|
+
// so `onOutcome` and `onEnded` are mutually exclusive even against a
|
|
112
|
+
// misbehaving surface.
|
|
113
|
+
let settled = null;
|
|
114
|
+
return {
|
|
115
|
+
focusedSessionId: session.focused_session_id,
|
|
116
|
+
conversationId: session.conversation_id,
|
|
117
|
+
name: session.name,
|
|
118
|
+
onOutcome(cb) {
|
|
119
|
+
return provider.onOutcome(event => {
|
|
120
|
+
if (event.focused_session_id !== session.focused_session_id)
|
|
121
|
+
return;
|
|
122
|
+
if (settled === 'ended')
|
|
123
|
+
return;
|
|
124
|
+
settled = 'outcome';
|
|
125
|
+
storeSessionId(null);
|
|
126
|
+
cb(event.outcome);
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
onEnded(cb) {
|
|
130
|
+
// Older surfaces without onEnded: a never-firing subscription (the
|
|
131
|
+
// page still compiles and runs; it simply keeps the legacy behavior).
|
|
132
|
+
if (!provider.onEnded)
|
|
133
|
+
return () => { };
|
|
134
|
+
return provider.onEnded(event => {
|
|
135
|
+
if (event.focused_session_id !== session.focused_session_id)
|
|
136
|
+
return;
|
|
137
|
+
if (settled === 'outcome')
|
|
138
|
+
return;
|
|
139
|
+
settled = 'ended';
|
|
140
|
+
storeSessionId(null);
|
|
141
|
+
cb({ focusedSessionId: event.focused_session_id, status: event.status });
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
end() {
|
|
145
|
+
storeSessionId(null);
|
|
146
|
+
return provider.end(session.focused_session_id);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ── Surviving a reload ───────────────────────────────────────────────────────
|
|
151
|
+
//
|
|
152
|
+
// A full browser reload destroys the page's handle while the session keeps
|
|
153
|
+
// running on the server, which used to strand the page (learning #17). The id
|
|
154
|
+
// is remembered here so the page can ask for it back.
|
|
155
|
+
//
|
|
156
|
+
// sessionStorage, not localStorage: a focused session belongs to the tab that
|
|
157
|
+
// started it, and it must not leak into a second tab where a different mission
|
|
158
|
+
// may be running. The read is NON-destructive; the entry is cleared only on a
|
|
159
|
+
// terminal event or an explicit call. A destructive read looks tempting and is
|
|
160
|
+
// a trap: under React StrictMode the double-invoked effect erases the value
|
|
161
|
+
// before the component that needed it ever sees it.
|
|
162
|
+
const RESUME_STORAGE_KEY = 'appilot.focused_session';
|
|
163
|
+
function readStoredSessionId() {
|
|
164
|
+
try {
|
|
165
|
+
const raw = window.sessionStorage?.getItem(RESUME_STORAGE_KEY);
|
|
166
|
+
return raw && raw.trim() !== '' ? raw : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Private mode, blocked storage, or a sandboxed frame. Resume simply is
|
|
170
|
+
// not available; nothing else degrades.
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function storeSessionId(id) {
|
|
175
|
+
try {
|
|
176
|
+
if (id)
|
|
177
|
+
window.sessionStorage?.setItem(RESUME_STORAGE_KEY, id);
|
|
178
|
+
else
|
|
179
|
+
window.sessionStorage?.removeItem(RESUME_STORAGE_KEY);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* storage unavailable: resume is not offered, the session still runs */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The id of the session this tab last started, if it has not reached a terminal
|
|
187
|
+
* state. Useful for deciding whether to offer a "continue" affordance before
|
|
188
|
+
* paying for the round-trip that `resumeFocusedSession` makes.
|
|
189
|
+
*/
|
|
190
|
+
export function getResumableFocusedSessionId() {
|
|
191
|
+
return getWindow() ? readStoredSessionId() : null;
|
|
192
|
+
}
|
|
193
|
+
/** Forget the remembered session without ending it server-side. */
|
|
194
|
+
export function forgetFocusedSession() {
|
|
195
|
+
if (getWindow())
|
|
196
|
+
storeSessionId(null);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Re-attach to a session this tab started before a reload.
|
|
200
|
+
*
|
|
201
|
+
* Resolves to a handle when the session is still running and belongs to the
|
|
202
|
+
* current identity, and to `null` otherwise: nothing remembered, the assistant
|
|
203
|
+
* surface cannot resume, the session already ended, or it is not this user's.
|
|
204
|
+
* Ownership is checked server-side; possessing an id proves nothing.
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* const handle = await resumeFocusedSession();
|
|
208
|
+
* if (handle) handle.onOutcome(saveEvidence);
|
|
209
|
+
* else showActivityFinishedState();
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export async function resumeFocusedSession(focusedSessionId) {
|
|
213
|
+
const id = focusedSessionId ?? getResumableFocusedSessionId();
|
|
214
|
+
if (!id)
|
|
215
|
+
return null;
|
|
216
|
+
try {
|
|
217
|
+
getWindow()?.dispatchEvent(new CustomEvent('appilot:open'));
|
|
218
|
+
}
|
|
219
|
+
catch { /* older browsers without CustomEvent; the eager-surface path still works */ }
|
|
220
|
+
let provider;
|
|
221
|
+
try {
|
|
222
|
+
provider = await waitForProvider(PROVIDER_WAIT_TIMEOUT_MS);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// No surface on this page. Resuming is a best-effort recovery, so it
|
|
226
|
+
// answers "no" rather than throwing at a page that is merely reloading.
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (!provider.resume)
|
|
230
|
+
return null;
|
|
231
|
+
const resumed = await provider.resume(id);
|
|
232
|
+
if (!resumed) {
|
|
233
|
+
storeSessionId(null);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
storeSessionId(resumed.focused_session_id);
|
|
237
|
+
return buildHandle(provider, resumed);
|
|
238
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP-proxy DOM tool. Lets the backend issue a fetch from inside the page the
|
|
3
|
+
* user is on so the host-application session cookie is carried automatically
|
|
4
|
+
* (`credentials: 'include'`), without the backend ever seeing a credential.
|
|
5
|
+
*
|
|
6
|
+
* Shared between the two in-page surfaces:
|
|
7
|
+
* - Chrome extension: invoked from the content script (relayed by the
|
|
8
|
+
* background service worker's AgentDomBridge).
|
|
9
|
+
* - Embeddable widget: invoked directly in-page by the widget's
|
|
10
|
+
* AgentDomBridge (the widget already runs inside the host page).
|
|
11
|
+
*
|
|
12
|
+
* Used by customer-authored Tools whose `runtime_spec.kind === 'http_proxy'`.
|
|
13
|
+
* The backend builds the URL + headers + body from the tool's runtime_spec
|
|
14
|
+
* (NOT from agent-supplied args), then asks the client to send it. Defence
|
|
15
|
+
* in depth on this side:
|
|
16
|
+
*
|
|
17
|
+
* 1. Same-origin enforcement against `location.origin`. Even if the backend
|
|
18
|
+
* asked us to hit `https://evil.example`, this responder rejects it.
|
|
19
|
+
* Relative paths only.
|
|
20
|
+
* 2. Method allowlist (GET / POST / PUT / DELETE).
|
|
21
|
+
* 3. AbortController-backed timeout, capped at 15 s.
|
|
22
|
+
* 4. 401/403 surfaces as a distinct `AUTH_REQUIRED` error so the agent loop
|
|
23
|
+
* can stop without nag-retrying.
|
|
24
|
+
* 5. No header echo: headers we forward are taken from the request payload
|
|
25
|
+
* and `credentials: 'include'` is FORCED. We do not write `Authorization`
|
|
26
|
+
* or `Cookie` even if the payload tries.
|
|
27
|
+
*
|
|
28
|
+
* See:
|
|
29
|
+
* - packages/libs/shared/src/types/agent/tools.ts § HttpProxyRuntimeSpec
|
|
30
|
+
* - docs/content-model/tools-http-proxy.md
|
|
31
|
+
*/
|
|
32
|
+
export interface HttpFetchArgs {
|
|
33
|
+
method?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
query?: Record<string, string>;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
body?: unknown;
|
|
38
|
+
timeout_ms?: number;
|
|
39
|
+
}
|
|
40
|
+
export type HttpFetchResult = {
|
|
41
|
+
ok: true;
|
|
42
|
+
status: number;
|
|
43
|
+
json: unknown;
|
|
44
|
+
duration_ms: number;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: 'BAD_REQUEST' | 'BLOCKED_ORIGIN' | 'BLOCKED_METHOD' | 'TIMEOUT' | 'NETWORK' | 'AUTH_REQUIRED' | 'STATUS' | 'PARSE';
|
|
48
|
+
detail?: string;
|
|
49
|
+
status?: number;
|
|
50
|
+
duration_ms?: number;
|
|
51
|
+
};
|
|
52
|
+
export declare function httpFetch(rawArgs: Record<string, unknown>): Promise<HttpFetchResult>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP-proxy DOM tool. Lets the backend issue a fetch from inside the page the
|
|
3
|
+
* user is on so the host-application session cookie is carried automatically
|
|
4
|
+
* (`credentials: 'include'`), without the backend ever seeing a credential.
|
|
5
|
+
*
|
|
6
|
+
* Shared between the two in-page surfaces:
|
|
7
|
+
* - Chrome extension: invoked from the content script (relayed by the
|
|
8
|
+
* background service worker's AgentDomBridge).
|
|
9
|
+
* - Embeddable widget: invoked directly in-page by the widget's
|
|
10
|
+
* AgentDomBridge (the widget already runs inside the host page).
|
|
11
|
+
*
|
|
12
|
+
* Used by customer-authored Tools whose `runtime_spec.kind === 'http_proxy'`.
|
|
13
|
+
* The backend builds the URL + headers + body from the tool's runtime_spec
|
|
14
|
+
* (NOT from agent-supplied args), then asks the client to send it. Defence
|
|
15
|
+
* in depth on this side:
|
|
16
|
+
*
|
|
17
|
+
* 1. Same-origin enforcement against `location.origin`. Even if the backend
|
|
18
|
+
* asked us to hit `https://evil.example`, this responder rejects it.
|
|
19
|
+
* Relative paths only.
|
|
20
|
+
* 2. Method allowlist (GET / POST / PUT / DELETE).
|
|
21
|
+
* 3. AbortController-backed timeout, capped at 15 s.
|
|
22
|
+
* 4. 401/403 surfaces as a distinct `AUTH_REQUIRED` error so the agent loop
|
|
23
|
+
* can stop without nag-retrying.
|
|
24
|
+
* 5. No header echo: headers we forward are taken from the request payload
|
|
25
|
+
* and `credentials: 'include'` is FORCED. We do not write `Authorization`
|
|
26
|
+
* or `Cookie` even if the payload tries.
|
|
27
|
+
*
|
|
28
|
+
* See:
|
|
29
|
+
* - packages/libs/shared/src/types/agent/tools.ts § HttpProxyRuntimeSpec
|
|
30
|
+
* - docs/content-model/tools-http-proxy.md
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
33
|
+
const MAX_TIMEOUT_MS = 15_000;
|
|
34
|
+
const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE']);
|
|
35
|
+
const FORBIDDEN_HEADER_NAMES = new Set(['authorization', 'cookie']);
|
|
36
|
+
export async function httpFetch(rawArgs) {
|
|
37
|
+
const args = rawArgs;
|
|
38
|
+
const method = String(args.method || 'GET').toUpperCase();
|
|
39
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
40
|
+
return { ok: false, error: 'BLOCKED_METHOD', detail: `method=${method}` };
|
|
41
|
+
}
|
|
42
|
+
const pathInput = typeof args.path === 'string' ? args.path : '';
|
|
43
|
+
if (!pathInput) {
|
|
44
|
+
return { ok: false, error: 'BAD_REQUEST', detail: 'missing path' };
|
|
45
|
+
}
|
|
46
|
+
// Same-origin enforcement. `new URL(pathInput, location.origin)` resolves a
|
|
47
|
+
// relative path against the current page origin. An absolute URL that points
|
|
48
|
+
// elsewhere will resolve with a different `.origin` and we reject it.
|
|
49
|
+
let target;
|
|
50
|
+
try {
|
|
51
|
+
target = new URL(pathInput, location.origin);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { ok: false, error: 'BAD_REQUEST', detail: `invalid path: ${String(err.message)}` };
|
|
55
|
+
}
|
|
56
|
+
if (target.origin !== location.origin) {
|
|
57
|
+
return { ok: false, error: 'BLOCKED_ORIGIN', detail: `target=${target.origin} active_tab=${location.origin}` };
|
|
58
|
+
}
|
|
59
|
+
if (args.query && typeof args.query === 'object') {
|
|
60
|
+
for (const [k, v] of Object.entries(args.query)) {
|
|
61
|
+
if (k && typeof v === 'string' && v.length > 0) {
|
|
62
|
+
target.searchParams.append(k, v);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const headers = new Headers();
|
|
67
|
+
if (args.headers && typeof args.headers === 'object') {
|
|
68
|
+
for (const [k, v] of Object.entries(args.headers)) {
|
|
69
|
+
if (FORBIDDEN_HEADER_NAMES.has(k.toLowerCase()))
|
|
70
|
+
continue;
|
|
71
|
+
if (typeof v === 'string')
|
|
72
|
+
headers.set(k, v);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!headers.has('Accept'))
|
|
76
|
+
headers.set('Accept', 'application/json');
|
|
77
|
+
const requestedTimeout = typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
|
78
|
+
? args.timeout_ms
|
|
79
|
+
: DEFAULT_TIMEOUT_MS;
|
|
80
|
+
const timeoutMs = Math.min(Math.max(requestedTimeout, 250), MAX_TIMEOUT_MS);
|
|
81
|
+
const init = {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
credentials: 'include',
|
|
85
|
+
redirect: 'follow',
|
|
86
|
+
};
|
|
87
|
+
if (method !== 'GET' && method !== 'DELETE' && args.body !== undefined) {
|
|
88
|
+
if (!headers.has('Content-Type'))
|
|
89
|
+
headers.set('Content-Type', 'application/json');
|
|
90
|
+
try {
|
|
91
|
+
init.body = typeof args.body === 'string' ? args.body : JSON.stringify(args.body);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return { ok: false, error: 'BAD_REQUEST', detail: `body serialise: ${err.message}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
init.signal = controller.signal;
|
|
99
|
+
const timeoutHandle = window.setTimeout(() => controller.abort(), timeoutMs);
|
|
100
|
+
const startedAt = performance.now();
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await fetch(target.toString(), init);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const elapsed = Math.round(performance.now() - startedAt);
|
|
107
|
+
if (controller.signal.aborted) {
|
|
108
|
+
return { ok: false, error: 'TIMEOUT', detail: `${timeoutMs}ms`, duration_ms: elapsed };
|
|
109
|
+
}
|
|
110
|
+
return { ok: false, error: 'NETWORK', detail: err.message, duration_ms: elapsed };
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
window.clearTimeout(timeoutHandle);
|
|
114
|
+
}
|
|
115
|
+
const elapsed = Math.round(performance.now() - startedAt);
|
|
116
|
+
if (response.status === 401 || response.status === 403) {
|
|
117
|
+
return { ok: false, error: 'AUTH_REQUIRED', status: response.status, duration_ms: elapsed };
|
|
118
|
+
}
|
|
119
|
+
let parsed;
|
|
120
|
+
const contentType = response.headers.get('content-type') || '';
|
|
121
|
+
const text = await response.text().catch(() => '');
|
|
122
|
+
if (contentType.includes('application/json') || (text && (text.startsWith('{') || text.startsWith('[')))) {
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(text);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return { ok: false, error: 'PARSE', status: response.status, detail: err.message, duration_ms: elapsed };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
parsed = { _text: text.slice(0, 4000) };
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
return { ok: false, error: 'STATUS', status: response.status, detail: typeof parsed === 'object' ? JSON.stringify(parsed).slice(0, 500) : String(parsed).slice(0, 500), duration_ms: elapsed };
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, status: response.status, json: parsed, duration_ms: elapsed };
|
|
137
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot`: the single developer-facing surface for building on Appilot.
|
|
3
|
+
*
|
|
4
|
+
* Four capabilities, one package:
|
|
5
|
+
* - Boot the assistant: bootAppilotWidget(...) (widget/boot)
|
|
6
|
+
* - Register client actions: registerTool(...) (WebMCP-aligned)
|
|
7
|
+
* - Run Focused Sessions: startFocusedSession(...) (mission-scoped)
|
|
8
|
+
* and resumeFocusedSession(...) to re-attach after a page reload
|
|
9
|
+
* - Coexist with your modals: isAppilotSurface(...) (dom/surfaces)
|
|
10
|
+
*
|
|
11
|
+
* Browser-only, React-free, zero runtime dependencies. The internal runtime the
|
|
12
|
+
* widget/extension use to FULFILL these calls lives at `appilot/runtime`.
|
|
13
|
+
*
|
|
14
|
+
* See the docs-site "Building on Appilot" guide and
|
|
15
|
+
* docs/content-model/tools-client-actions.md.
|
|
16
|
+
*/
|
|
17
|
+
export { bootAppilotWidget, getWidgetAvailability, subscribeWidgetAvailability, type WidgetAvailability, type AppilotWidgetBootOptions, } from './widget/boot.js';
|
|
18
|
+
export { registerTool, registerClientTool, unregisterClientTool, onClientToolsChange, hasNativeWebMcp, type ClientToolDefinition, type ClientToolHandle, } from './webmcp/sdk.js';
|
|
19
|
+
export type { ClientToolContent, ClientToolResult } from './webmcp/registry.js';
|
|
20
|
+
export { isAppilotSurface, markAppilotSurface, APPILOT_SURFACE_ATTR, type AppilotSurfaceKind, } from './dom/surfaces.js';
|
|
21
|
+
export { startFocusedSession, resumeFocusedSession, getResumableFocusedSessionId, forgetFocusedSession, type FocusedSessionPageHandle, } from './focusedSessions/pageSdk.js';
|
|
22
|
+
export type { FocusedSessionMission, FocusedSessionStartRequest, FocusedSessionStartResult, FocusedSessionOutcomeEvent, FocusedSessionPresentation, FocusedSessionEndStatus, FocusedSessionEndedEvent, } from './sessions/types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot`: the single developer-facing surface for building on Appilot.
|
|
3
|
+
*
|
|
4
|
+
* Four capabilities, one package:
|
|
5
|
+
* - Boot the assistant: bootAppilotWidget(...) (widget/boot)
|
|
6
|
+
* - Register client actions: registerTool(...) (WebMCP-aligned)
|
|
7
|
+
* - Run Focused Sessions: startFocusedSession(...) (mission-scoped)
|
|
8
|
+
* and resumeFocusedSession(...) to re-attach after a page reload
|
|
9
|
+
* - Coexist with your modals: isAppilotSurface(...) (dom/surfaces)
|
|
10
|
+
*
|
|
11
|
+
* Browser-only, React-free, zero runtime dependencies. The internal runtime the
|
|
12
|
+
* widget/extension use to FULFILL these calls lives at `appilot/runtime`.
|
|
13
|
+
*
|
|
14
|
+
* See the docs-site "Building on Appilot" guide and
|
|
15
|
+
* docs/content-model/tools-client-actions.md.
|
|
16
|
+
*/
|
|
17
|
+
// --- Boot the assistant -----------------------------------------------------
|
|
18
|
+
export { bootAppilotWidget, getWidgetAvailability, subscribeWidgetAvailability, } from './widget/boot.js';
|
|
19
|
+
// --- Client actions (WebMCP-aligned) ---------------------------------------
|
|
20
|
+
export { registerTool, registerClientTool, unregisterClientTool, onClientToolsChange, hasNativeWebMcp, } from './webmcp/sdk.js';
|
|
21
|
+
// --- Coexisting with the host app's own modal layers ------------------------
|
|
22
|
+
export { isAppilotSurface, markAppilotSurface, APPILOT_SURFACE_ATTR, } from './dom/surfaces.js';
|
|
23
|
+
// --- Focused Sessions -------------------------------------------------------
|
|
24
|
+
export { startFocusedSession, resumeFocusedSession, getResumableFocusedSessionId, forgetFocusedSession, } from './focusedSessions/pageSdk.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot/runtime`: INTERNAL runtime primitives the Appilot widget and
|
|
3
|
+
* browser-extension content scripts use to FULFILL the public SDK calls
|
|
4
|
+
* (register/collect/serialize/invoke client tools, execute the DOM tools +
|
|
5
|
+
* `http_fetch`, and back the Focused Sessions transport).
|
|
6
|
+
*
|
|
7
|
+
* This entry is NOT part of the public developer surface: host apps import from
|
|
8
|
+
* `appilot`. It is exposed as a subpath so the assistant surfaces (which
|
|
9
|
+
* DO ship this logic) import it without pulling it into the public barrel.
|
|
10
|
+
*/
|
|
11
|
+
export { runDomTool, resetDomToolTurn, type DomToolRequest } from './domResponder.js';
|
|
12
|
+
export { httpFetch, type HttpFetchArgs, type HttpFetchResult } from './httpFetch.js';
|
|
13
|
+
export { collectClientTools } from './webmcp/sdk.js';
|
|
14
|
+
export { invokeClientTool, serializeClientTools, normalizeResult, __resetClientToolRegistry, type SerializedClientTool, } from './webmcp/registry.js';
|
|
15
|
+
export { registerFocusedSessionProvider, type FocusedSessionProvider, } from './focusedSessions/pageSdk.js';
|
|
16
|
+
export { isSensitiveField, isSensitiveFieldType } from './dom/sensitiveFields.js';
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot/runtime`: INTERNAL runtime primitives the Appilot widget and
|
|
3
|
+
* browser-extension content scripts use to FULFILL the public SDK calls
|
|
4
|
+
* (register/collect/serialize/invoke client tools, execute the DOM tools +
|
|
5
|
+
* `http_fetch`, and back the Focused Sessions transport).
|
|
6
|
+
*
|
|
7
|
+
* This entry is NOT part of the public developer surface: host apps import from
|
|
8
|
+
* `appilot`. It is exposed as a subpath so the assistant surfaces (which
|
|
9
|
+
* DO ship this logic) import it without pulling it into the public barrel.
|
|
10
|
+
*/
|
|
11
|
+
// DOM tools + HTTP-proxy executor (the agent_dom_request round-trip).
|
|
12
|
+
export { runDomTool, resetDomToolTurn } from './domResponder.js';
|
|
13
|
+
export { httpFetch } from './httpFetch.js';
|
|
14
|
+
// WebMCP client-action collection / serialization / invocation.
|
|
15
|
+
export { collectClientTools } from './webmcp/sdk.js';
|
|
16
|
+
export { invokeClientTool, serializeClientTools, normalizeResult, __resetClientToolRegistry, } from './webmcp/registry.js';
|
|
17
|
+
// Focused Sessions transport provider (implemented by the assistant surface).
|
|
18
|
+
export { registerFocusedSessionProvider, } from './focusedSessions/pageSdk.js';
|
|
19
|
+
// The one rule about credential fields. Re-exported by `appilot-shared/scanning`
|
|
20
|
+
// for the surfaces that already import it from there.
|
|
21
|
+
export { isSensitiveField, isSensitiveFieldType } from './dom/sensitiveFields.js';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused Sessions: the public type contract for the page-facing SDK surface
|
|
3
|
+
* (`startFocusedSession` and its handle). These are the types a host app touches
|
|
4
|
+
* when it starts an app-initiated, mission-scoped agent conversation and receives
|
|
5
|
+
* its structured outcome. See docs/agent/focused-sessions.md.
|
|
6
|
+
*
|
|
7
|
+
* This file is the SINGLE SOURCE OF TRUTH for the SDK-facing session types.
|
|
8
|
+
* `appilot-shared` re-exports these (type-only) from `appilot-shared/types` so
|
|
9
|
+
* backend/repository code keeps its existing import path; the mission/template
|
|
10
|
+
* cluster that only the backend needs stays in `appilot-shared`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The RESOLVED mission frame a session runs under (the purely-runtime channel;
|
|
14
|
+
* the DB-anchored channel composes this server-side from a Session Template).
|
|
15
|
+
* Untrusted when page-supplied: sanitized + capped + gated by deployment config.
|
|
16
|
+
*/
|
|
17
|
+
export interface FocusedSessionMission {
|
|
18
|
+
/** Display name for the session header ("Role-play: unhappy customer"). */
|
|
19
|
+
name: string;
|
|
20
|
+
/**
|
|
21
|
+
* The mission prompt: function, persona, boundaries and completion criteria,
|
|
22
|
+
* with variable slots already substituted.
|
|
23
|
+
*/
|
|
24
|
+
prompt: string;
|
|
25
|
+
/**
|
|
26
|
+
* App/client tool names the session may use, on top of a minimal internal
|
|
27
|
+
* set. Empty/absent = no app or client tools. A session can only narrow the
|
|
28
|
+
* catalog, never widen it past what the tenant already has.
|
|
29
|
+
*/
|
|
30
|
+
allowed_tools?: string[];
|
|
31
|
+
/** Optional KB doc-id allowlist; absent = KB tools see the normal tenant scope. */
|
|
32
|
+
kb_doc_ids?: string[];
|
|
33
|
+
/** JSON Schema the `complete_session` outcome payload must satisfy. */
|
|
34
|
+
outcome_schema: Record<string, unknown>;
|
|
35
|
+
/** Turn budget; the session is abandoned server-side when exceeded. */
|
|
36
|
+
max_turns?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* How the assistant surface PRESENTS a focused session.
|
|
40
|
+
*
|
|
41
|
+
* A mission-scoped conversation is a different genre from ambient assistance,
|
|
42
|
+
* and the runtime has always known that (mission, closed tool scope,
|
|
43
|
+
* lifecycle, structured outcome). The UI did not: a role-play ran inside the
|
|
44
|
+
* ambient chrome plus a chip, so an end user saw a KB coverage diagnostic, an
|
|
45
|
+
* input that said "Ask anything about this page", and a one-click "New
|
|
46
|
+
* conversation" that silently orphaned the activity.
|
|
47
|
+
*
|
|
48
|
+
* THIS IS NOT A LAYOUT LANGUAGE, and keeping it that way is the point. Every
|
|
49
|
+
* field is a closed union or a plain string of COPY; none is a color, a size,
|
|
50
|
+
* a layout, a component, or markup. The rule for adding one: it must be
|
|
51
|
+
* justifiable for at least three unrelated app genres (a role-play, a support
|
|
52
|
+
* triage, a guided onboarding). A field only one app would ever set does not
|
|
53
|
+
* belong in a platform contract. A host that needs more than this renders it
|
|
54
|
+
* on its own page and lets the session drive it; an open presentation contract
|
|
55
|
+
* would just rebuild per-app bespoke UI in JSON, which is the cost Appilot
|
|
56
|
+
* exists to remove.
|
|
57
|
+
*/
|
|
58
|
+
export interface FocusedSessionPresentation {
|
|
59
|
+
/**
|
|
60
|
+
* `immersive` suppresses ambient-assistance furniture that is meaningless
|
|
61
|
+
* or harmful inside a mission: the KB coverage chip (an app-owner
|
|
62
|
+
* diagnostic), the welcome tip, and the "New conversation" affordance.
|
|
63
|
+
* Defaults to `ambient`, so every existing session is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
chrome?: 'ambient' | 'immersive';
|
|
66
|
+
/**
|
|
67
|
+
* `agent` runs one opening turn at start, so the assistant speaks first.
|
|
68
|
+
* For an activity this is the difference between a chat window and
|
|
69
|
+
* something that has begun: a learner should not have to type "hello" at a
|
|
70
|
+
* persona to make a role-play start. Defaults to `user`.
|
|
71
|
+
*/
|
|
72
|
+
opening?: 'agent' | 'user';
|
|
73
|
+
/** Replaces "Ask anything about this page..." (e.g. "Reply to Mr. Weber"). */
|
|
74
|
+
input_placeholder?: string;
|
|
75
|
+
/** Relabels the session exit affordance (e.g. "End activity"). */
|
|
76
|
+
exit_label?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Page SDK -> backend: start a focused session. Exactly one of template_id / mission. */
|
|
79
|
+
export interface FocusedSessionStartRequest {
|
|
80
|
+
/** DB-anchored channel: the Session Template to instantiate. */
|
|
81
|
+
template_id?: string;
|
|
82
|
+
/** Purely-runtime channel (dev/demos; gated by deployment config). */
|
|
83
|
+
mission?: FocusedSessionMission;
|
|
84
|
+
/** Values for the template's declared variable slots. */
|
|
85
|
+
variables?: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
export interface FocusedSessionStartResult {
|
|
88
|
+
focused_session_id: string;
|
|
89
|
+
/** The conversation the session is bound to (1:1, never rotates). */
|
|
90
|
+
conversation_id: number;
|
|
91
|
+
/** Resolved display name for the session header. */
|
|
92
|
+
name: string;
|
|
93
|
+
/**
|
|
94
|
+
* How the assistant surface should present this session. Resolved
|
|
95
|
+
* server-side from the template; absent means ambient defaults.
|
|
96
|
+
*/
|
|
97
|
+
presentation?: FocusedSessionPresentation;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Payload of the `session_outcome` SSE event and of the host-page callback.
|
|
101
|
+
* Emitted once, on the turn where the agent calls `complete_session` with a
|
|
102
|
+
* schema-valid outcome. The outcome is AI output: apps must run it through their
|
|
103
|
+
* own review model, never treat it as human-validated truth.
|
|
104
|
+
*/
|
|
105
|
+
export interface FocusedSessionOutcomeEvent {
|
|
106
|
+
focused_session_id: string;
|
|
107
|
+
status: 'completed';
|
|
108
|
+
/** The schema-validated structured result. */
|
|
109
|
+
outcome: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* How a session reached a terminal state WITHOUT delivering an outcome
|
|
113
|
+
* (the `onEnded` page callback):
|
|
114
|
+
* - `abandoned`: the server abandoned it (turn budget exhausted, or an
|
|
115
|
+
* out-of-band end) and the assistant surface learned it from the typed
|
|
116
|
+
* stream error of the refused turn;
|
|
117
|
+
* - `failed`: the mission could not be fulfilled and no outcome will ever
|
|
118
|
+
* exist for it. Emitted server-side as the typed `SESSION_FAILED` stream
|
|
119
|
+
* error. Distinct from `abandoned`, which means the session ran out of
|
|
120
|
+
* room rather than out of premise;
|
|
121
|
+
* - `expired`: reserved for a future session TTL; surfaces map unknown
|
|
122
|
+
* terminal causes here;
|
|
123
|
+
* - `ended`: the page itself called `handle.end()`.
|
|
124
|
+
*
|
|
125
|
+
* NOTE the asymmetry with a mission that fails its PREFLIGHT: that session is
|
|
126
|
+
* never created, so it has no terminal status at all. `startFocusedSession`
|
|
127
|
+
* rejects instead, and the page renders its own error state. See
|
|
128
|
+
* docs/agent/focused-sessions.md § Preflight.
|
|
129
|
+
*/
|
|
130
|
+
export type FocusedSessionEndStatus = 'abandoned' | 'expired' | 'failed' | 'ended';
|
|
131
|
+
/**
|
|
132
|
+
* Payload of the page-facing `onEnded` callback: the session reached a terminal
|
|
133
|
+
* state WITHOUT having delivered an outcome. Fired exactly once per session, and
|
|
134
|
+
* NEVER for a session that delivered a `session_outcome` (`onOutcome` and
|
|
135
|
+
* `onEnded` are mutually exclusive).
|
|
136
|
+
*/
|
|
137
|
+
export interface FocusedSessionEndedEvent {
|
|
138
|
+
focused_session_id: string;
|
|
139
|
+
status: FocusedSessionEndStatus;
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused Sessions: the public type contract for the page-facing SDK surface
|
|
3
|
+
* (`startFocusedSession` and its handle). These are the types a host app touches
|
|
4
|
+
* when it starts an app-initiated, mission-scoped agent conversation and receives
|
|
5
|
+
* its structured outcome. See docs/agent/focused-sessions.md.
|
|
6
|
+
*
|
|
7
|
+
* This file is the SINGLE SOURCE OF TRUTH for the SDK-facing session types.
|
|
8
|
+
* `appilot-shared` re-exports these (type-only) from `appilot-shared/types` so
|
|
9
|
+
* backend/repository code keeps its existing import path; the mission/template
|
|
10
|
+
* cluster that only the backend needs stays in `appilot-shared`.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|