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.
@@ -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
+ }
@@ -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';
@@ -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 {};