appilot-mcp 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/LICENSE +15 -0
  4. package/README.md +133 -27
  5. package/dist/appilot-configurator.mcpb +0 -0
  6. package/dist/cli.d.ts +34 -0
  7. package/dist/cli.js +173 -0
  8. package/dist/client.d.ts +45 -1
  9. package/dist/client.js +74 -1
  10. package/dist/config.d.ts +21 -0
  11. package/dist/config.js +6 -0
  12. package/dist/contract/healthContract.js +31 -4
  13. package/dist/index.bundle.js +2111 -826
  14. package/dist/index.d.ts +7 -1
  15. package/dist/index.js +22 -4
  16. package/dist/manifest.d.ts +14 -2
  17. package/dist/manifest.js +31 -9
  18. package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
  19. package/dist/public-marketplace/README.md +23 -0
  20. package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
  21. package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
  22. package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
  23. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
  24. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
  25. package/dist/redaction.d.ts +51 -0
  26. package/dist/redaction.js +59 -0
  27. package/dist/remote/consent.d.ts +10 -2
  28. package/dist/remote/consent.js +16 -6
  29. package/dist/remote/consentMessages.d.ts +6 -1
  30. package/dist/remote/consentMessages.js +15 -6
  31. package/dist/remote/httpServer.d.ts +10 -0
  32. package/dist/remote/httpServer.js +126 -40
  33. package/dist/remote/oauth.d.ts +10 -1
  34. package/dist/remote/oauth.js +29 -11
  35. package/dist/scaffold.d.ts +68 -6
  36. package/dist/scaffold.js +424 -97
  37. package/dist/server.js +175 -18
  38. package/dist/userClient.d.ts +213 -0
  39. package/dist/userClient.js +400 -0
  40. package/dist/userServer.d.ts +47 -0
  41. package/dist/userServer.js +248 -0
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/examples/app.appilot.json +212 -0
  45. package/mcpb/manifest.json +117 -21
  46. package/package.json +5 -3
  47. package/skills/app-configurator/SKILL.md +61 -19
package/dist/server.js CHANGED
@@ -19,7 +19,7 @@ import { inspectPage } from './inspect.js';
19
19
  import { runHealthContract } from './contract/healthContract.js';
20
20
  import { soakSelectors } from './soak.js';
21
21
  import { applyManifest, parseManifest, planManifest } from './manifest.js';
22
- import { scaffoldIntegration, scaffoldAgentFirst } from './scaffold.js';
22
+ import { SCAFFOLD_FRAMEWORKS, scaffoldIntegration, scaffoldAgentFirst, integrationSnippet } from './scaffold.js';
23
23
  import { verifyIntegration } from './verify.js';
24
24
  import { redactForTransport, refuseSecretOverRemote } from './redaction.js';
25
25
  /**
@@ -43,7 +43,9 @@ const DEFAULT_WIDGET_SCRIPT_URL = 'https://cdn.appilot.space/widget/v1/appilot.e
43
43
  */
44
44
  const SERVER_INSTRUCTIONS = `Audit, extend and fix an Appilot app's content-model configuration, and build a new capability so the agent can operate it.
45
45
 
46
- Work in this order: capabilities (what this instance supports, and the closed vocabularies its entities accept; on-premise trails cloud), read_config, validate_config, then report the findings to the user in plain language, ranked critical to low, each with its concrete fix. Do not paste raw tool output at them.
46
+ For a new app, work in this order: whoami, then create_app (on bare localhost pass isTest, which mints a wk_test_ key), then scaffold_integration for the host's framework, then verify_integration against the running page, then configure the content model.
47
+
48
+ To audit an existing one, work in this order: capabilities (what this instance supports, and the closed vocabularies its entities accept; on-premise trails cloud), read_config, validate_config, then report the findings to the user in plain language, ranked critical to low, each with its concrete fix. Do not paste raw tool output at them.
47
49
 
48
50
  If read_config returns a gaps array, an entity could not be read. Say so and stop treating that entity as empty: every lint over it silently passed.
49
51
 
@@ -58,6 +60,20 @@ Three things to get right. A widget secret belongs in the server environment and
58
60
  If something is missing or broken in Appilot itself, report_feedback records it. Say plainly what it does: the report is read, and a reply is part of a support plan rather than something promised here. Never put configuration contents, knowledge bodies or secrets in a report, and show the user the exact text first.
59
61
 
60
62
  Writing needs a config:write service token, reading needs config:read, provisioning needs provision:write, reporting needs feedback:write. A scope refusal means the user should reconnect with a token carrying that scope, not that you should find another route.`;
63
+ /**
64
+ * Where each scope comes from, appended to the refusal.
65
+ *
66
+ * A refusal that names the missing scope and stops leaves the reader one
67
+ * question short of acting: who can widen it. For provisioning the answer is
68
+ * not "you", which is exactly the case the configurator meets, so the refusal
69
+ * says who to ask and what the preset is called on the screen they will open.
70
+ */
71
+ const HOW_TO_GRANT = {
72
+ 'config:read': 'config:read is on every Service tokens preset in the Backoffice, "Inspect only" included.',
73
+ 'config:write': 'config:write comes from the Backoffice Service tokens preset "Edit configuration".',
74
+ 'provision:write': 'provision:write is granted by an organization administrator in the Backoffice, under Service tokens with the "Set up integrations" preset, or through the account approval screen. A configurator who is not an administrator cannot mint it and has to ask one.',
75
+ 'feedback:write': 'feedback:write is a Backoffice Service tokens option, and it is the only scope that sends anything out of the organization.',
76
+ };
61
77
  export function createAppilotServer(conn) {
62
78
  const client = new AppilotClient(conn);
63
79
  function text(value) {
@@ -85,7 +101,8 @@ export function createAppilotServer(conn) {
85
101
  if (!granted || granted.includes(scope))
86
102
  return null;
87
103
  return errorText(new Error(`This connection was granted ${granted.length ? granted.join(', ') : 'no scopes'}, which does not include ${scope}. ` +
88
- 'Reconnect and approve that scope, using a service token that carries it.'));
104
+ 'Reconnect and approve that scope, using a service token that carries it. ' +
105
+ HOW_TO_GRANT[scope]));
89
106
  }
90
107
  function resolveAppId(appId) {
91
108
  const id = appId ?? conn.defaultAppId;
@@ -365,16 +382,17 @@ export function createAppilotServer(conn) {
365
382
  });
366
383
  server.registerTool('create_app', {
367
384
  title: 'Create an app, its domains, and a widget key',
368
- description: 'Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Requires a provision:write service token.',
385
+ description: 'Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Working on your own machine: do NOT pass localhost or 127.0.0.1 as a domain, because they name every developer\'s machine and are refused. Either run the app on a hostname that resolves to 127.0.0.1 (myapp.lvh.me) and register THAT, which gives the turn full app context and needs no key, or pass isTest true with no domains for a loopback-bound wk_test_ key, which gives the tenant and the user and no app context. Requires a provision:write service token.',
369
386
  inputSchema: {
370
387
  name: z.string().min(1),
371
388
  description: z.string().optional(),
372
389
  domains: z.array(z.string()).optional(),
373
390
  widgetKeyName: z.string().optional(),
374
- isTestKey: z.boolean().optional(),
391
+ isTest: z.boolean().optional(),
392
+ allowedDomains: z.array(z.string()).optional(),
375
393
  dryRun: z.boolean().optional(),
376
394
  },
377
- }, async ({ name, description, domains, widgetKeyName, isTestKey, dryRun }) => {
395
+ }, async ({ name, description, domains, widgetKeyName, isTest, allowedDomains, dryRun }) => {
378
396
  const refusal = scopeRefusal('provision:write');
379
397
  if (refusal)
380
398
  return refusal;
@@ -382,8 +400,8 @@ export function createAppilotServer(conn) {
382
400
  const result = await client.provisionApp({
383
401
  app: { name, description },
384
402
  domains: (domains ?? []).map(domain => ({ domain })),
385
- widgetKey: widgetKeyName || isTestKey !== undefined
386
- ? { name: widgetKeyName, isTest: isTestKey }
403
+ widgetKey: widgetKeyName || isTest !== undefined || allowedDomains
404
+ ? { name: widgetKeyName, isTest, allowedDomains }
387
405
  : undefined,
388
406
  dryRun: dryRun === true,
389
407
  });
@@ -393,9 +411,135 @@ export function createAppilotServer(conn) {
393
411
  return errorText(err);
394
412
  }
395
413
  });
414
+ server.registerTool('list_apps', {
415
+ title: 'List the apps this organization has provisioned',
416
+ description: 'The apps this credential reaches, with each app\'s registered domains and their verification status. Call it before create_app so "which apps do I have" has an answer, and to find the app id and the domain id every other provisioning tool takes. Carries no secret. Reads the provisioning surface, so it needs a provision:write service token even though it writes nothing.',
417
+ inputSchema: {},
418
+ }, async () => {
419
+ const refusal = scopeRefusal('provision:write');
420
+ if (refusal)
421
+ return refusal;
422
+ try {
423
+ return text(await client.listProvisioned());
424
+ }
425
+ catch (err) {
426
+ return errorText(err);
427
+ }
428
+ });
429
+ server.registerTool('list_widget_keys', {
430
+ title: 'List the widget keys this organization holds',
431
+ description: 'The organization\'s widget keys by label, prefix, allowed domains and active state. No raw key and no secret: both are shown exactly once, at creation. Call it before create_app so a re-run recognises the key it already minted instead of asking for another, because every extra key is another live credential. Requires a provision:write service token.',
432
+ inputSchema: {},
433
+ }, async () => {
434
+ const refusal = scopeRefusal('provision:write');
435
+ if (refusal)
436
+ return refusal;
437
+ try {
438
+ return text(await client.listWidgetKeys());
439
+ }
440
+ catch (err) {
441
+ return errorText(err);
442
+ }
443
+ });
444
+ server.registerTool('verify_domain', {
445
+ title: 'Check or trigger a domain\'s DNS verification',
446
+ description: 'Where a domain\'s verification stands, and the exact TXT record it needs. Pass trigger to ask Appilot to look for the record now. A live widget key needs a verified domain, and this is what closes that loop: create_app reports the key as blocked and names the record, and this reports whether the record is visible yet. Requires a provision:write service token.',
447
+ inputSchema: {
448
+ domain: z.string().min(1),
449
+ appId: z.number().int().optional(),
450
+ trigger: z.boolean().optional(),
451
+ },
452
+ }, async ({ domain, appId, trigger }) => {
453
+ const refusal = scopeRefusal('provision:write');
454
+ if (refusal)
455
+ return refusal;
456
+ try {
457
+ const wanted = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/[/:].*$/, '');
458
+ const provisioned = await client.listProvisioned();
459
+ const scoped = appId != null ? provisioned.filter(a => Number(a.id) === appId) : provisioned;
460
+ const owner = scoped.find(a => a.domains.some(d => String(d.domain).toLowerCase() === wanted));
461
+ const row = owner?.domains.find(d => String(d.domain).toLowerCase() === wanted);
462
+ if (!owner || !row) {
463
+ return errorText(new Error(`${wanted} is not a registered domain of ${appId != null ? `app ${appId}` : 'any app in this organization'}. ` +
464
+ 'list_apps shows what is registered, and create_app adds a domain. A loopback name (localhost, 127.0.0.1) is never registrable: run the app on a hostname that resolves to 127.0.0.1 instead.'));
465
+ }
466
+ const result = {
467
+ app: { id: owner.id, name: owner.name },
468
+ domain: row.domain,
469
+ domainId: row.id,
470
+ verificationStatus: row.verification_status,
471
+ };
472
+ // The TXT record lives on the apps router, which authenticates an
473
+ // organization session rather than a service token. When that is
474
+ // refused, the provisioning dry run answers the same question, so the
475
+ // tool degrades to a second source instead of to nothing.
476
+ try {
477
+ result.record = await client.domainVerification(Number(row.id));
478
+ }
479
+ catch {
480
+ try {
481
+ const preview = await client.provisionApp({
482
+ app: { name: owner.name },
483
+ domains: [{ domain: row.domain }],
484
+ dryRun: true,
485
+ });
486
+ const previewed = preview.domains.find(d => d.domain.toLowerCase() === wanted);
487
+ if (previewed?.dns_record_name) {
488
+ result.record = {
489
+ dns_record_name: previewed.dns_record_name,
490
+ dns_record_value: previewed.dns_record_value,
491
+ verification_status: previewed.verification_status,
492
+ };
493
+ }
494
+ }
495
+ catch (err) {
496
+ result.recordUnavailable = err instanceof Error ? err.message : String(err);
497
+ }
498
+ }
499
+ if (trigger && row.verification_status !== 'verified') {
500
+ try {
501
+ result.checked = await client.triggerDomainVerification(Number(row.id));
502
+ }
503
+ catch (err) {
504
+ result.triggerRefused =
505
+ `${err instanceof Error ? err.message : String(err)} ` +
506
+ 'Triggering the check authenticates an organization session rather than a service token today, so run it from the Backoffice under Domains once the TXT record is published.';
507
+ }
508
+ }
509
+ return text(result);
510
+ }
511
+ catch (err) {
512
+ return errorText(err);
513
+ }
514
+ });
515
+ server.registerTool('integration_snippet', {
516
+ title: 'Get the script tag and boot call for an app that already exists',
517
+ description: 'The two forms of the widget boot for an app you already provisioned: the no-build script tag, and the bundled bootAppilotWidget call reading the key from the framework\'s public variable. Use it instead of re-running create_app, which is a provisioning write, when all you lost was the snippet. It composes the answer locally, writes nothing, and needs no scope. Pass the publishable widget key to have it appear in the script tag; the widget SECRET never belongs here.',
518
+ inputSchema: {
519
+ appId: z.number().int().optional(),
520
+ widgetKey: z.string().optional(),
521
+ widgetScriptUrl: z.string().optional(),
522
+ framework: z.enum(SCAFFOLD_FRAMEWORKS).optional(),
523
+ },
524
+ }, async ({ appId, widgetKey, widgetScriptUrl, framework }) => {
525
+ try {
526
+ return text({
527
+ appId: appId ?? conn.defaultAppId ?? null,
528
+ ...integrationSnippet({
529
+ widgetScriptUrl: widgetScriptUrl ?? DEFAULT_WIDGET_SCRIPT_URL,
530
+ apiUrl: conn.baseUrl || null,
531
+ widgetKey: widgetKey ?? null,
532
+ framework,
533
+ }),
534
+ });
535
+ }
536
+ catch (err) {
537
+ return errorText(err);
538
+ }
539
+ });
396
540
  server.registerTool('plan_manifest', {
397
541
  title: 'Preview an app manifest (writes nothing)',
398
- description: 'Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control.',
542
+ description: 'Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control. A complete example ships with this package at examples/app.appilot.json and is also in the docs. Which half is revisioned: the CONFIG half gets a pre_restore revision on every commit, so a wrong import is undone by restoring it; the PROVISIONING half is not revisioned, so a domain or a key it creates is undone by hand.',
399
543
  inputSchema: { manifest: z.union([z.record(z.any()), z.string()]) },
400
544
  }, async ({ manifest }) => {
401
545
  const refusal = scopeRefusal('config:read');
@@ -415,7 +559,7 @@ export function createAppilotServer(conn) {
415
559
  });
416
560
  server.registerTool('apply_manifest', {
417
561
  title: 'Apply a previously planned app manifest',
418
- description: 'Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions. Requires provision:write, and config:write when the manifest carries a config bundle.',
562
+ description: 'Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions. Needs config:write when the manifest carries a config bundle, and provision:write only when the provisioning half would actually change something: a manifest whose app, domains and keys already exist applies with config:write alone.',
419
563
  inputSchema: {
420
564
  manifest: z.union([z.record(z.any()), z.string()]),
421
565
  planToken: z.string(),
@@ -424,17 +568,26 @@ export function createAppilotServer(conn) {
424
568
  allowUnhealthy: z.boolean().optional(),
425
569
  },
426
570
  }, async ({ manifest, planToken, mode, expectedCurrentHash, allowUnhealthy }) => {
427
- // A manifest apply provisions AND writes configuration, so it needs both.
428
- const refusal = scopeRefusal('provision:write') ?? scopeRefusal('config:write');
429
- if (refusal)
430
- return refusal;
431
571
  try {
432
572
  const parsed = parseManifest(manifest);
573
+ // The config half always writes when the manifest carries one. The
574
+ // provisioning half is decided by the manifest rather than by the
575
+ // tool: applyManifest previews it and refuses only when it would
576
+ // actually change an app, a domain or a key. A CI token holding
577
+ // config:write alone keeps content in sync on an app a person
578
+ // already provisioned, which is the reason the scopes are separate.
579
+ if (parsed.config) {
580
+ const configRefusal = scopeRefusal('config:write');
581
+ if (configRefusal)
582
+ return configRefusal;
583
+ }
584
+ const provisionRefusal = scopeRefusal('provision:write');
433
585
  const result = await applyManifest(client, parsed, {
434
586
  planToken,
435
587
  mode,
436
588
  expectedCurrentHash,
437
589
  allowUnhealthy,
590
+ provisionRefusal: provisionRefusal ? provisionRefusal.content[0].text : null,
438
591
  });
439
592
  return text({ ...result, provisioning: redactForTransport(result.provisioning, conn.transport) });
440
593
  }
@@ -444,9 +597,9 @@ export function createAppilotServer(conn) {
444
597
  });
445
598
  server.registerTool('scaffold_integration', {
446
599
  title: 'Generate the host application integration code',
447
- description: 'Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host.',
600
+ description: 'Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host, or `other` when the backend is not Node (Django, Rails, PHP), which returns the raw exchange as curl plus a Python and a Ruby handler. The notes carry what local development needs, and the answer differs between a hostname you registered and bare localhost.',
448
601
  inputSchema: {
449
- framework: z.enum(['next', 'express', 'fastify', 'hono', 'remix', 'sveltekit']),
602
+ framework: z.enum(SCAFFOLD_FRAMEWORKS),
450
603
  widgetKey: z.string().optional(),
451
604
  widgetScriptUrl: z.string().optional(),
452
605
  idNamespace: z.string().optional(),
@@ -522,15 +675,17 @@ export function createAppilotServer(conn) {
522
675
  });
523
676
  server.registerTool('scaffold_agent_first', {
524
677
  title: 'Scaffold one capability so the agent can operate it',
525
- description: 'Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Use it when building a new agent-first app or making an existing feature reachable through the assistant.',
678
+ description: 'Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Pass `shape`: `create` is the open, fill and submit plan, `navigate` is one step to a screen, and `read` gets NO plan at all, because a plan whose only step opens a screen does nothing. Pass `clientSide` when the operation belongs in the page, and no HTTP-proxy tool is emitted. Use it when building a new agent-first app or making an existing feature reachable through the assistant.',
526
679
  inputSchema: {
527
680
  capability: z.string().min(1),
528
681
  slug: z.string().min(1),
529
682
  appId: z.number().int().optional(),
530
683
  endpoint: z.object({ method: z.string(), path: z.string() }).optional(),
531
684
  clientSide: z.boolean().optional(),
685
+ shape: z.enum(['create', 'navigate', 'read']).optional(),
686
+ viewPath: z.string().optional(),
532
687
  },
533
- }, async ({ capability, slug, appId, endpoint, clientSide }) => {
688
+ }, async ({ capability, slug, appId, endpoint, clientSide, shape, viewPath }) => {
534
689
  try {
535
690
  return text(scaffoldAgentFirst({
536
691
  capability,
@@ -538,6 +693,8 @@ export function createAppilotServer(conn) {
538
693
  appId: appId ?? conn.defaultAppId ?? null,
539
694
  endpoint: endpoint ?? null,
540
695
  clientSide,
696
+ shape,
697
+ viewPath,
541
698
  }));
542
699
  }
543
700
  catch (err) {
@@ -0,0 +1,213 @@
1
+ /**
2
+ * HTTP client over the Appilot runtime routes (`/agent/runtime/*`), used by the
3
+ * Appilot connector: the surface an end user operates their own app through,
4
+ * from whatever assistant they already pay for.
5
+ *
6
+ * The credential is a user session, not a service token. It authenticates the
7
+ * way the extension does, so the connector can never exceed what the person
8
+ * themselves can do, and it can never write configuration: that is Studio, on a
9
+ * different mount with a different scope (see `client.ts`).
10
+ *
11
+ * Two rules about what this client does with an answer.
12
+ *
13
+ * Page-derived content passes through VERBATIM. `page/read` and `page/find`
14
+ * answer with one `page_content` string: the DOM tool's result serialized and
15
+ * wrapped in the backend's delimited untrusted-content block. Nothing here opens
16
+ * that block. The delimiters are a prompt-injection defence that works only
17
+ * while they are still around the content, the model reads the JSON inside them
18
+ * perfectly well, and a client that reassembled structure out of the block would
19
+ * be a second parser of a hostile string.
20
+ *
21
+ * Everything that is NOT page-derived is read field by field, and never invented.
22
+ * A missing field does not throw: an answer the connector cannot parse must not
23
+ * take the conversation down. A failed read never becomes an empty success:
24
+ * `configured` stays null when the instance did not say, an absent plan list is
25
+ * not an empty one, and a refusal comes back as a refusal carrying the guidance
26
+ * the backend wrote for the person.
27
+ *
28
+ * Contract: docs/architecture/appilot-runtime-connector.md.
29
+ * Routes: packages/services/backend/src/routes/agentRuntime.ts.
30
+ * Channel: docs/architecture/agent-bridge.md.
31
+ */
32
+ import type { AppilotConnection } from './config.js';
33
+ /**
34
+ * What to say when a refusal arrives without guidance of its own.
35
+ *
36
+ * The routes send `error.guidance` from the bridge's failure vocabulary and
37
+ * that is what the assistant repeats, so this map is the fallback for a body
38
+ * that carries none. `NO_BRIDGE` is the one that matters most: a caller that
39
+ * turns it into a guess about the page is the failure the vocabulary exists to
40
+ * prevent.
41
+ */
42
+ export declare const BRIDGE_REFUSALS: Record<string, string>;
43
+ /** A typed refusal from the runtime routes. Never a value a caller can mistake for data. */
44
+ export declare class RuntimeRefusalError extends Error {
45
+ readonly code: string;
46
+ readonly status: number;
47
+ constructor(message: string, code: string, status: number);
48
+ }
49
+ /**
50
+ * The steer that keeps an assistant from narrating a plan the person is already
51
+ * watching. `runtimeEnvelope` puts it on every response; see the connector spec,
52
+ * "Every result says whether it was shown".
53
+ */
54
+ export interface Presentation {
55
+ shown_in_page: boolean;
56
+ instruction?: string;
57
+ }
58
+ /** What a visible result should have said, repeated when a result forgets to. */
59
+ export declare const SHOWN_IN_PAGE_INSTRUCTION = "This ran in the page the person is watching. Confirm in one sentence. Do not list the steps.";
60
+ /** Mode and its reason, exactly as the server derived them. Never inflated here. */
61
+ export interface ModeStamp {
62
+ mode: string | null;
63
+ mode_reason: string | null;
64
+ }
65
+ export interface RuntimeContextResult extends ModeStamp {
66
+ bridge_live: boolean | null;
67
+ surface: string | null;
68
+ url: string | null;
69
+ app: {
70
+ id: number | null;
71
+ name: string | null;
72
+ } | null;
73
+ view: {
74
+ view_path: string | null;
75
+ view_name: string | null;
76
+ } | null;
77
+ /** Null when the instance did not say. Never defaulted, in either direction. */
78
+ configured: boolean | null;
79
+ configured_reason: string | null;
80
+ /** Null when the instance did not answer with a plan list at all. */
81
+ plans: Record<string, unknown>[] | null;
82
+ presentation: Presentation | null;
83
+ notes: string[];
84
+ }
85
+ /**
86
+ * A page read or find. `page_content` is the backend's delimited untrusted block
87
+ * and travels exactly as it arrived.
88
+ */
89
+ export interface RuntimePageResult extends ModeStamp {
90
+ what?: string;
91
+ page_content: string | null;
92
+ elapsed_ms: number | null;
93
+ presentation: Presentation | null;
94
+ notes: string[];
95
+ }
96
+ export interface RuntimeHighlightResult extends ModeStamp {
97
+ target: Record<string, unknown> | null;
98
+ elapsed_ms: number | null;
99
+ presentation: Presentation | null;
100
+ notes: string[];
101
+ }
102
+ export interface RuntimeKnowledgeResult extends ModeStamp {
103
+ /** Null when the instance did not answer with passages. Empty means it found none. */
104
+ passages: Record<string, unknown>[] | null;
105
+ citations: Record<string, unknown>[] | null;
106
+ configured: boolean | null;
107
+ configured_reason: string | null;
108
+ /** The server's own line for the person, on an app with no curated knowledge. */
109
+ guidance: string | null;
110
+ presentation: Presentation | null;
111
+ notes: string[];
112
+ }
113
+ export interface RuntimePlansResult extends ModeStamp {
114
+ plans: Record<string, unknown>[] | null;
115
+ configured: boolean | null;
116
+ view_path: string | null;
117
+ presentation: Presentation | null;
118
+ notes: string[];
119
+ }
120
+ export interface RuntimePlanRunResult extends ModeStamp {
121
+ plan_handle: string | null;
122
+ plan_id: string | null;
123
+ plan_name: string | null;
124
+ interaction_id: number | null;
125
+ pace: string | null;
126
+ state: string | null;
127
+ /** Above zero means the assembler refused part of what was asked. */
128
+ steps_dropped: number | null;
129
+ presentation: Presentation | null;
130
+ notes: string[];
131
+ }
132
+ export interface RuntimePlanStatusResult extends ModeStamp {
133
+ plan_handle: string | null;
134
+ plan_id: string | null;
135
+ plan_name: string | null;
136
+ pace: string | null;
137
+ state: string | null;
138
+ step_index: number | null;
139
+ total_steps: number | null;
140
+ detail: string | null;
141
+ updated_at: number | null;
142
+ /** True when the call held for a transition rather than reading the current state. */
143
+ waited: boolean;
144
+ presentation: Presentation | null;
145
+ notes: string[];
146
+ }
147
+ /** The three paces the dock implements. There is no fourth, and a connector may not invent timings. */
148
+ export declare const PACES: readonly ["teach", "walk", "do"];
149
+ export type Pace = (typeof PACES)[number];
150
+ /** The three ways `POST /page/read` can look at the page, in the route's own words. */
151
+ export declare const PAGE_READS: readonly ["outline", "text", "form_state"];
152
+ export type PageRead = (typeof PAGE_READS)[number];
153
+ /** One field of a plan's form step, by the control's semantic id. */
154
+ export interface PlanFormValue {
155
+ control: string;
156
+ value: string;
157
+ }
158
+ export declare class AppilotRuntimeClient {
159
+ private readonly conn;
160
+ constructor(conn: AppilotConnection);
161
+ private request;
162
+ /** Where the person is, and what applies here. Carries no mode by design. */
163
+ context(): Promise<RuntimeContextResult>;
164
+ /**
165
+ * Outline, visible text in a region, or form state with validation errors.
166
+ *
167
+ * The answer's `page_content` is passed on untouched, delimiters and all.
168
+ */
169
+ readPage(args: {
170
+ what: PageRead;
171
+ region_id?: string;
172
+ selector?: string;
173
+ form_id?: string;
174
+ max_chars?: number;
175
+ }): Promise<RuntimePageResult>;
176
+ /** Elements by role and accessible name, inside the same untrusted block. */
177
+ findOnPage(args: {
178
+ role?: string;
179
+ name?: string;
180
+ limit?: number;
181
+ }): Promise<RuntimePageResult>;
182
+ private pageResult;
183
+ /** Draw the locate overlay on a control, a zone or a ref. Visible work. */
184
+ highlight(args: {
185
+ control?: string;
186
+ zone?: string;
187
+ ref?: string;
188
+ }): Promise<RuntimeHighlightResult>;
189
+ /** Curated knowledge with resolved citations. Answers without a shared tab. */
190
+ searchKnowledge(args: {
191
+ query: string;
192
+ }): Promise<RuntimeKnowledgeResult>;
193
+ /** The curated procedures that apply to the current view. */
194
+ listPlans(): Promise<RuntimePlansResult>;
195
+ /**
196
+ * Assemble a curated plan and publish it to the page at a pace.
197
+ *
198
+ * `plan_id` is the authored id `list_plans` returns; there is no free-form
199
+ * goal, because an app with no curated plan for the task is refused rather
200
+ * than served an assembled guess.
201
+ */
202
+ runPlan(args: {
203
+ plan_id: string;
204
+ pace?: Pace;
205
+ form_values?: PlanFormValue[];
206
+ }): Promise<RuntimePlanRunResult>;
207
+ /**
208
+ * State of a running plan. With `wait` the route holds up to 50 seconds for
209
+ * the next transition, which is how an assistant that cannot see the screen
210
+ * follows a plan without polling.
211
+ */
212
+ planStatus(handle: string, wait?: boolean): Promise<RuntimePlanStatusResult>;
213
+ }