@stina/extension-api 1.3.1 → 1.6.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.
@@ -1178,9 +1178,22 @@ interface ModelCapabilities {
1178
1178
  * Report this per model *and* per auth mode, like `voiceDuplex`: the same
1179
1179
  * provider often serves both a vision model and a text-only one, and a picture
1180
1180
  * sent to the latter is at best ignored and at worst an error mid-conversation.
1181
- * Stina uses it to decide whether the paperclip is offered at all.
1182
1181
  */
1183
1182
  vision?: boolean;
1183
+ /**
1184
+ * The model can be given files to read alongside the text of a message, and
1185
+ * the provider folds {@link ChatMessage.files} into whatever its API calls them.
1186
+ *
1187
+ * Separate from `vision` because the two come apart in both directions: a model
1188
+ * that reads a PDF natively is not necessarily one that looks at a photograph,
1189
+ * and an OpenAI-compatible server with a vision model behind it may take images
1190
+ * and nothing else. Report it per model and per auth mode for the same reason
1191
+ * `vision` is reported that way.
1192
+ *
1193
+ * A provider that says nothing here keeps behaving exactly as before: it is
1194
+ * handed the text, and `files` is simply a field it does not read.
1195
+ */
1196
+ documents?: boolean;
1184
1197
  }
1185
1198
  /**
1186
1199
  * How the client wants to connect to the voice session.
@@ -1280,6 +1293,18 @@ interface ChatMessage {
1280
1293
  * Only ever `image/jpeg` or `image/png` — see `ChatAttachmentDTO` for why.
1281
1294
  */
1282
1295
  images?: ChatImage[];
1296
+ /**
1297
+ * Files the user attached for the model to read: PDFs and plain text.
1298
+ *
1299
+ * Additive in the same way `images` is, and split from it for the same reason
1300
+ * the two capabilities are separate — a provider folds a document into a
1301
+ * different content block than a picture, and many can do one and not the other.
1302
+ * A provider that ignores the field behaves exactly as it did before.
1303
+ *
1304
+ * Only present on user messages, because that is where both hosted providers
1305
+ * require a document to sit.
1306
+ */
1307
+ files?: ChatFile[];
1283
1308
  /** For assistant messages: tool calls made by the model */
1284
1309
  tool_calls?: ToolCall[];
1285
1310
  /** For tool messages: the ID of the tool call this is a response to */
@@ -1299,6 +1324,29 @@ interface ChatImage {
1299
1324
  /** The image itself, base64 with no data-URI prefix. */
1300
1325
  data: string;
1301
1326
  }
1327
+ /**
1328
+ * One file the user attached for the model to read, rather than to look at.
1329
+ *
1330
+ * `application/pdf` and `text/plain` are what the host stores, so those are what
1331
+ * arrive. Both hosted providers read a PDF natively and want it as its own content
1332
+ * block; plain text needs no such thing and can simply be put in the prompt, which
1333
+ * is why a provider with no document support at all can still do something useful
1334
+ * with a `text/plain` file if it chooses to.
1335
+ */
1336
+ interface ChatFile {
1337
+ /** `application/pdf` or `text/plain`. */
1338
+ mime: string;
1339
+ /** The file itself, base64 with no data-URI prefix. */
1340
+ data: string;
1341
+ /**
1342
+ * The name the file arrived under, when it had one.
1343
+ *
1344
+ * Worth passing on rather than dropping: `faktura-1042.pdf` is most of what is
1345
+ * known about a file before it is opened, and both providers have somewhere to
1346
+ * put it — a file name on the one, a document title on the other.
1347
+ */
1348
+ name?: string;
1349
+ }
1302
1350
  /**
1303
1351
  * A tool call made by the model
1304
1352
  */
@@ -1714,6 +1762,46 @@ interface ExecutionContext {
1714
1762
  readonly secrets: SecretsAPI;
1715
1763
  /** User-scoped secrets */
1716
1764
  readonly userSecrets: SecretsAPI;
1765
+ /**
1766
+ * The files attached to this user's conversations.
1767
+ *
1768
+ * Present only for an extension holding `attachments.read`, and only on a request
1769
+ * that knows whose it is. Absent otherwise, so a tool that wants files has to say
1770
+ * so in its manifest and check before reaching for them.
1771
+ */
1772
+ readonly attachments?: AttachmentsAPI;
1773
+ }
1774
+ /**
1775
+ * Reading a file that is already in a conversation.
1776
+ *
1777
+ * For handing one on: mailing back the PDF she was just shown, printing it, putting
1778
+ * it somewhere. Not for finding out what it says — `core_read_attachment` does that
1779
+ * without an extension, and getting the text is nearly always what she actually
1780
+ * wants.
1781
+ */
1782
+ interface AttachmentsAPI {
1783
+ /**
1784
+ * Read one attachment by id.
1785
+ *
1786
+ * The id comes from Stina, which is the whole point: she gets it from the message
1787
+ * a file arrived on, or from the result of the tool that handed it over, and
1788
+ * passes it to a tool as a parameter.
1789
+ *
1790
+ * Resolves `null` when no such attachment belongs to this user — deleted, or
1791
+ * never theirs. The two are the same answer on purpose.
1792
+ */
1793
+ read(attachmentId: string): Promise<AttachmentContent | null>;
1794
+ }
1795
+ /** One attachment's bytes, with what is known about them. */
1796
+ interface AttachmentContent {
1797
+ id: string;
1798
+ /** `image/jpeg`, `image/png`, `application/pdf` or `text/plain`, read from the bytes. */
1799
+ mime: string;
1800
+ /** base64, no data-URI prefix. */
1801
+ data: string;
1802
+ byteSize: number;
1803
+ /** The name it was stored under, when it had one. */
1804
+ name?: string;
1717
1805
  }
1718
1806
  /**
1719
1807
  * Context provided to extension's activate function.
@@ -2353,30 +2441,44 @@ interface ToolResult {
2353
2441
  */
2354
2442
  cardSuggestion?: string;
2355
2443
  /**
2356
- * Pictures to show the user, rendered in the conversation where the tool ran.
2444
+ * Files to put in front of the user, in the conversation where the tool ran.
2357
2445
  *
2358
- * For what a card cannot hold and the model cannot reproduce: a generated
2359
- * image, a rendered document, a photo fetched on the user's behalf. Like
2360
- * {@link ToolResult.display}, this is for the user only — it is lifted out
2361
- * before the result reaches the model, which would have nothing to do with
2362
- * the bytes but spend tokens on them. Say in `data` that a picture was shown,
2363
- * so she can talk about it without describing it back.
2446
+ * For what a card cannot hold and the model cannot reproduce: a generated image,
2447
+ * a photo fetched on the user's behalf, the PDF that came attached to a mail.
2448
+ * Like {@link ToolResult.display}, this is for the user — it is lifted out before
2449
+ * the result reaches the model, which would have nothing to do with the bytes but
2450
+ * spend tokens on them. Say in `data` that a file was attached, so she can talk
2451
+ * about it without reciting it.
2364
2452
  *
2365
- * The host stores each one as an attachment of the conversation, the same way
2366
- * a picture the user sends is stored, so it is served to every client and can
2367
- * be saved or shared from there. JPEG and PNG only, and 20 MB at most, since
2368
- * those are the limits the attachment store already holds users to. One that
2369
- * fails them is dropped with a warning rather than half-shown.
2453
+ * The host stores each one as an attachment of the conversation, exactly as a
2454
+ * file the user sends is stored, so it is served to every client and can be saved
2455
+ * or shared from there. The store's own rules apply: JPEG and PNG, PDF, and plain
2456
+ * text, up to 20 MB (1 MB for text). What the file *is* is read from the bytes,
2457
+ * not from `name`, so a PDF mislabelled `.png` still lands as a PDF. One that
2458
+ * fails the rules is dropped with a warning rather than half-shown.
2459
+ *
2460
+ * Attaching a file does not read it to the model. What comes back in the result
2461
+ * the model sees is a reference apiece — `{ id, mime, name? }` under this same
2462
+ * key, in place of the bytes — and she reads one with `core_read_attachment` if
2463
+ * she decides to. That is the intended flow for a mail's attachment: hand it over
2464
+ * here, and let her choose whether to open it.
2370
2465
  */
2371
2466
  attachments?: ToolAttachment[];
2372
2467
  }
2373
2468
  /**
2374
- * One picture a tool wants shown. See {@link ToolResult.attachments}.
2469
+ * One file a tool wants to put in the conversation. See {@link ToolResult.attachments}.
2375
2470
  */
2376
2471
  interface ToolAttachment {
2377
- /** The image bytes, base64 encoded. The format is read from the bytes. */
2472
+ /** The bytes, base64 encoded. What they are is read from them, not from `name`. */
2378
2473
  data: string;
2379
- /** A file name to offer when the user saves it, e.g. `friday.png`. */
2474
+ /**
2475
+ * A file name to offer when the user saves it, e.g. `friday.png` or
2476
+ * `faktura-1042.pdf`.
2477
+ *
2478
+ * Worth sending for a picture and close to required for a document: it is the
2479
+ * whole label the user sees in the conversation, and it is what Stina has to go
2480
+ * on when she decides whether to read it.
2481
+ */
2380
2482
  name?: string;
2381
2483
  }
2382
2484
  /**
@@ -2405,4 +2507,4 @@ interface ActionResult {
2405
2507
  error?: string;
2406
2508
  }
2407
2509
 
2408
- export { type BackgroundTaskContext as $, type ActionResult as A, type ActionsAPI as B, type ChatMessage as C, type Disposable as D, type ExtensionContributions as E, type EventsAPI as F, type GetModelsOptions as G, type HugeIconName as H, type SchedulerAPI as I, type SchedulerJobRequest as J, type SchedulerSchedule as K, type LocalizedString as L, type ModelInfo as M, type NetworkAPI as N, type UserProfile as O, type PanelDefinition as P, type ChatAPI as Q, type ChatInstructionMessage as R, type SchedulerFirePayload as S, type ToolResult as T, type UserAPI as U, type VoiceSessionOptions as V, type ConversationPresentation as W, type LogAPI as X, type BackgroundWorkersAPI as Y, type BackgroundTaskConfig as Z, type BackgroundTaskCallback as _, type ChatOptions as a, type WeatherForecastProps as a$, type BackgroundTaskHealth as a0, type BackgroundRestartPolicy as a1, type Query as a2, type QueryOptions as a3, type StorageAPI as a4, type SecretsAPI as a5, type StorageCollectionConfig as a6, type StorageContributions as a7, type AIProvider as a8, type ModelCapabilities as a9, type SelectProps as aA, type IconPickerProps as aB, type VerticalStackProps as aC, type HorizontalStackProps as aD, type GridProps as aE, type DividerProps as aF, type IconProps as aG, type IconButtonType as aH, type IconButtonProps as aI, type PanelAction as aJ, type PanelProps as aK, type ToggleProps as aL, type CollapsibleProps as aM, type FrameVariant as aN, type FrameProps as aO, type ListProps as aP, type PillVariant as aQ, type PillProps as aR, type CheckboxProps as aS, type MarkdownProps as aT, type TextPreviewProps as aU, type ModalProps as aV, type ConditionalGroupProps as aW, type WeatherCondition as aX, type WeatherWind as aY, type WeatherNowProps as aZ, type WeatherForecastStep as a_, type ChatImage as aa, type ToolCall as ab, type VoiceTransportRequest as ac, type Tool as ad, type ToolAttachment as ae, type Action as af, type ExtensionModule as ag, type AllowedCSSProperty as ah, type ExtensionComponentStyle as ai, type ExtensionComponentData as aj, type ExtensionComponentIterator as ak, type ExtensionComponentChildren as al, type ExtensionActionCall as am, type ExtensionActionRef as an, type ExtensionDataSource as ao, type ExtensionPanelDefinition as ap, type HeaderProps as aq, type LabelProps as ar, type ClockProps as as, type ParagraphProps as at, type ButtonProps as au, type TextInputProps as av, type PasswordInputProps as aw, type NumberInputProps as ax, type TextAreaProps as ay, type DateTimeInputProps as az, type StreamEvent as b, type ChartKind as b0, type ChartSeries as b1, type ChartProps as b2, type StatTrend as b3, type StatTileProps as b4, type ProgressShape as b5, type ProgressColor as b6, type ProgressBarProps as b7, type KeyValueRow as b8, type KeyValueListProps as b9, type TimelineVariant as ba, type TimelineEntry as bb, type TimelineProps as bc, type NoteVariant as bd, type NoteProps as be, type CalendarEventStatus as bf, type CalendarEventProps as bg, type ExecutionContext as bh, type VoiceSessionDescriptor as c, type ToolSettingsViewDefinition as d, type ToolSettingsView as e, type ToolSettingsListView as f, type ToolSettingsListMapping as g, type ToolSettingsComponentView as h, type ToolSettingsActionDataSource as i, type StatusCardDefinition as j, type PanelView as k, type PanelComponentView as l, type PanelActionDataSource as m, type PanelUnknownView as n, type ProviderDefinition as o, type ProviderConfigView as p, type PromptContribution as q, resolveLocalizedString as r, type PromptSection as s, type ToolDefinition as t, type ToolConfirmationConfig as u, type CommandDefinition as v, type ExtensionContext as w, type SettingsAPI as x, type ProvidersAPI as y, type ToolsAPI as z };
2510
+ export { type BackgroundTaskConfig as $, type ActionResult as A, type ActionsAPI as B, type ChatMessage as C, type Disposable as D, type ExtensionContributions as E, type EventsAPI as F, type GetModelsOptions as G, type HugeIconName as H, type SchedulerAPI as I, type SchedulerJobRequest as J, type SchedulerSchedule as K, type LocalizedString as L, type ModelInfo as M, type NetworkAPI as N, type UserProfile as O, type PanelDefinition as P, type ChatAPI as Q, type ChatInstructionMessage as R, type SchedulerFirePayload as S, type ToolResult as T, type UserAPI as U, type VoiceSessionOptions as V, type ConversationPresentation as W, type AttachmentsAPI as X, type AttachmentContent as Y, type LogAPI as Z, type BackgroundWorkersAPI as _, type ChatOptions as a, type WeatherWind as a$, type BackgroundTaskCallback as a0, type BackgroundTaskContext as a1, type BackgroundTaskHealth as a2, type BackgroundRestartPolicy as a3, type Query as a4, type QueryOptions as a5, type StorageAPI as a6, type SecretsAPI as a7, type StorageCollectionConfig as a8, type StorageContributions as a9, type NumberInputProps as aA, type TextAreaProps as aB, type DateTimeInputProps as aC, type SelectProps as aD, type IconPickerProps as aE, type VerticalStackProps as aF, type HorizontalStackProps as aG, type GridProps as aH, type DividerProps as aI, type IconProps as aJ, type IconButtonType as aK, type IconButtonProps as aL, type PanelAction as aM, type PanelProps as aN, type ToggleProps as aO, type CollapsibleProps as aP, type FrameVariant as aQ, type FrameProps as aR, type ListProps as aS, type PillVariant as aT, type PillProps as aU, type CheckboxProps as aV, type MarkdownProps as aW, type TextPreviewProps as aX, type ModalProps as aY, type ConditionalGroupProps as aZ, type WeatherCondition as a_, type AIProvider as aa, type ModelCapabilities as ab, type ChatImage as ac, type ChatFile as ad, type ToolCall as ae, type VoiceTransportRequest as af, type Tool as ag, type ToolAttachment as ah, type Action as ai, type ExtensionModule as aj, type AllowedCSSProperty as ak, type ExtensionComponentStyle as al, type ExtensionComponentData as am, type ExtensionComponentIterator as an, type ExtensionComponentChildren as ao, type ExtensionActionCall as ap, type ExtensionActionRef as aq, type ExtensionDataSource as ar, type ExtensionPanelDefinition as as, type HeaderProps as at, type LabelProps as au, type ClockProps as av, type ParagraphProps as aw, type ButtonProps as ax, type TextInputProps as ay, type PasswordInputProps as az, type StreamEvent as b, type WeatherNowProps as b0, type WeatherForecastStep as b1, type WeatherForecastProps as b2, type ChartKind as b3, type ChartSeries as b4, type ChartProps as b5, type StatTrend as b6, type StatTileProps as b7, type ProgressShape as b8, type ProgressColor as b9, type ProgressBarProps as ba, type KeyValueRow as bb, type KeyValueListProps as bc, type TimelineVariant as bd, type TimelineEntry as be, type TimelineProps as bf, type NoteVariant as bg, type NoteProps as bh, type CalendarEventStatus as bi, type CalendarEventProps as bj, type ExecutionContext as bk, type VoiceSessionDescriptor as c, type ToolSettingsViewDefinition as d, type ToolSettingsView as e, type ToolSettingsListView as f, type ToolSettingsListMapping as g, type ToolSettingsComponentView as h, type ToolSettingsActionDataSource as i, type StatusCardDefinition as j, type PanelView as k, type PanelComponentView as l, type PanelActionDataSource as m, type PanelUnknownView as n, type ProviderDefinition as o, type ProviderConfigView as p, type PromptContribution as q, resolveLocalizedString as r, type PromptSection as s, type ToolDefinition as t, type ToolConfirmationConfig as u, type CommandDefinition as v, type ExtensionContext as w, type SettingsAPI as x, type ProvidersAPI as y, type ToolsAPI as z };
@@ -1178,9 +1178,22 @@ interface ModelCapabilities {
1178
1178
  * Report this per model *and* per auth mode, like `voiceDuplex`: the same
1179
1179
  * provider often serves both a vision model and a text-only one, and a picture
1180
1180
  * sent to the latter is at best ignored and at worst an error mid-conversation.
1181
- * Stina uses it to decide whether the paperclip is offered at all.
1182
1181
  */
1183
1182
  vision?: boolean;
1183
+ /**
1184
+ * The model can be given files to read alongside the text of a message, and
1185
+ * the provider folds {@link ChatMessage.files} into whatever its API calls them.
1186
+ *
1187
+ * Separate from `vision` because the two come apart in both directions: a model
1188
+ * that reads a PDF natively is not necessarily one that looks at a photograph,
1189
+ * and an OpenAI-compatible server with a vision model behind it may take images
1190
+ * and nothing else. Report it per model and per auth mode for the same reason
1191
+ * `vision` is reported that way.
1192
+ *
1193
+ * A provider that says nothing here keeps behaving exactly as before: it is
1194
+ * handed the text, and `files` is simply a field it does not read.
1195
+ */
1196
+ documents?: boolean;
1184
1197
  }
1185
1198
  /**
1186
1199
  * How the client wants to connect to the voice session.
@@ -1280,6 +1293,18 @@ interface ChatMessage {
1280
1293
  * Only ever `image/jpeg` or `image/png` — see `ChatAttachmentDTO` for why.
1281
1294
  */
1282
1295
  images?: ChatImage[];
1296
+ /**
1297
+ * Files the user attached for the model to read: PDFs and plain text.
1298
+ *
1299
+ * Additive in the same way `images` is, and split from it for the same reason
1300
+ * the two capabilities are separate — a provider folds a document into a
1301
+ * different content block than a picture, and many can do one and not the other.
1302
+ * A provider that ignores the field behaves exactly as it did before.
1303
+ *
1304
+ * Only present on user messages, because that is where both hosted providers
1305
+ * require a document to sit.
1306
+ */
1307
+ files?: ChatFile[];
1283
1308
  /** For assistant messages: tool calls made by the model */
1284
1309
  tool_calls?: ToolCall[];
1285
1310
  /** For tool messages: the ID of the tool call this is a response to */
@@ -1299,6 +1324,29 @@ interface ChatImage {
1299
1324
  /** The image itself, base64 with no data-URI prefix. */
1300
1325
  data: string;
1301
1326
  }
1327
+ /**
1328
+ * One file the user attached for the model to read, rather than to look at.
1329
+ *
1330
+ * `application/pdf` and `text/plain` are what the host stores, so those are what
1331
+ * arrive. Both hosted providers read a PDF natively and want it as its own content
1332
+ * block; plain text needs no such thing and can simply be put in the prompt, which
1333
+ * is why a provider with no document support at all can still do something useful
1334
+ * with a `text/plain` file if it chooses to.
1335
+ */
1336
+ interface ChatFile {
1337
+ /** `application/pdf` or `text/plain`. */
1338
+ mime: string;
1339
+ /** The file itself, base64 with no data-URI prefix. */
1340
+ data: string;
1341
+ /**
1342
+ * The name the file arrived under, when it had one.
1343
+ *
1344
+ * Worth passing on rather than dropping: `faktura-1042.pdf` is most of what is
1345
+ * known about a file before it is opened, and both providers have somewhere to
1346
+ * put it — a file name on the one, a document title on the other.
1347
+ */
1348
+ name?: string;
1349
+ }
1302
1350
  /**
1303
1351
  * A tool call made by the model
1304
1352
  */
@@ -1714,6 +1762,46 @@ interface ExecutionContext {
1714
1762
  readonly secrets: SecretsAPI;
1715
1763
  /** User-scoped secrets */
1716
1764
  readonly userSecrets: SecretsAPI;
1765
+ /**
1766
+ * The files attached to this user's conversations.
1767
+ *
1768
+ * Present only for an extension holding `attachments.read`, and only on a request
1769
+ * that knows whose it is. Absent otherwise, so a tool that wants files has to say
1770
+ * so in its manifest and check before reaching for them.
1771
+ */
1772
+ readonly attachments?: AttachmentsAPI;
1773
+ }
1774
+ /**
1775
+ * Reading a file that is already in a conversation.
1776
+ *
1777
+ * For handing one on: mailing back the PDF she was just shown, printing it, putting
1778
+ * it somewhere. Not for finding out what it says — `core_read_attachment` does that
1779
+ * without an extension, and getting the text is nearly always what she actually
1780
+ * wants.
1781
+ */
1782
+ interface AttachmentsAPI {
1783
+ /**
1784
+ * Read one attachment by id.
1785
+ *
1786
+ * The id comes from Stina, which is the whole point: she gets it from the message
1787
+ * a file arrived on, or from the result of the tool that handed it over, and
1788
+ * passes it to a tool as a parameter.
1789
+ *
1790
+ * Resolves `null` when no such attachment belongs to this user — deleted, or
1791
+ * never theirs. The two are the same answer on purpose.
1792
+ */
1793
+ read(attachmentId: string): Promise<AttachmentContent | null>;
1794
+ }
1795
+ /** One attachment's bytes, with what is known about them. */
1796
+ interface AttachmentContent {
1797
+ id: string;
1798
+ /** `image/jpeg`, `image/png`, `application/pdf` or `text/plain`, read from the bytes. */
1799
+ mime: string;
1800
+ /** base64, no data-URI prefix. */
1801
+ data: string;
1802
+ byteSize: number;
1803
+ /** The name it was stored under, when it had one. */
1804
+ name?: string;
1717
1805
  }
1718
1806
  /**
1719
1807
  * Context provided to extension's activate function.
@@ -2353,30 +2441,44 @@ interface ToolResult {
2353
2441
  */
2354
2442
  cardSuggestion?: string;
2355
2443
  /**
2356
- * Pictures to show the user, rendered in the conversation where the tool ran.
2444
+ * Files to put in front of the user, in the conversation where the tool ran.
2357
2445
  *
2358
- * For what a card cannot hold and the model cannot reproduce: a generated
2359
- * image, a rendered document, a photo fetched on the user's behalf. Like
2360
- * {@link ToolResult.display}, this is for the user only — it is lifted out
2361
- * before the result reaches the model, which would have nothing to do with
2362
- * the bytes but spend tokens on them. Say in `data` that a picture was shown,
2363
- * so she can talk about it without describing it back.
2446
+ * For what a card cannot hold and the model cannot reproduce: a generated image,
2447
+ * a photo fetched on the user's behalf, the PDF that came attached to a mail.
2448
+ * Like {@link ToolResult.display}, this is for the user — it is lifted out before
2449
+ * the result reaches the model, which would have nothing to do with the bytes but
2450
+ * spend tokens on them. Say in `data` that a file was attached, so she can talk
2451
+ * about it without reciting it.
2364
2452
  *
2365
- * The host stores each one as an attachment of the conversation, the same way
2366
- * a picture the user sends is stored, so it is served to every client and can
2367
- * be saved or shared from there. JPEG and PNG only, and 20 MB at most, since
2368
- * those are the limits the attachment store already holds users to. One that
2369
- * fails them is dropped with a warning rather than half-shown.
2453
+ * The host stores each one as an attachment of the conversation, exactly as a
2454
+ * file the user sends is stored, so it is served to every client and can be saved
2455
+ * or shared from there. The store's own rules apply: JPEG and PNG, PDF, and plain
2456
+ * text, up to 20 MB (1 MB for text). What the file *is* is read from the bytes,
2457
+ * not from `name`, so a PDF mislabelled `.png` still lands as a PDF. One that
2458
+ * fails the rules is dropped with a warning rather than half-shown.
2459
+ *
2460
+ * Attaching a file does not read it to the model. What comes back in the result
2461
+ * the model sees is a reference apiece — `{ id, mime, name? }` under this same
2462
+ * key, in place of the bytes — and she reads one with `core_read_attachment` if
2463
+ * she decides to. That is the intended flow for a mail's attachment: hand it over
2464
+ * here, and let her choose whether to open it.
2370
2465
  */
2371
2466
  attachments?: ToolAttachment[];
2372
2467
  }
2373
2468
  /**
2374
- * One picture a tool wants shown. See {@link ToolResult.attachments}.
2469
+ * One file a tool wants to put in the conversation. See {@link ToolResult.attachments}.
2375
2470
  */
2376
2471
  interface ToolAttachment {
2377
- /** The image bytes, base64 encoded. The format is read from the bytes. */
2472
+ /** The bytes, base64 encoded. What they are is read from them, not from `name`. */
2378
2473
  data: string;
2379
- /** A file name to offer when the user saves it, e.g. `friday.png`. */
2474
+ /**
2475
+ * A file name to offer when the user saves it, e.g. `friday.png` or
2476
+ * `faktura-1042.pdf`.
2477
+ *
2478
+ * Worth sending for a picture and close to required for a document: it is the
2479
+ * whole label the user sees in the conversation, and it is what Stina has to go
2480
+ * on when she decides whether to read it.
2481
+ */
2380
2482
  name?: string;
2381
2483
  }
2382
2484
  /**
@@ -2405,4 +2507,4 @@ interface ActionResult {
2405
2507
  error?: string;
2406
2508
  }
2407
2509
 
2408
- export { type BackgroundTaskContext as $, type ActionResult as A, type ActionsAPI as B, type ChatMessage as C, type Disposable as D, type ExtensionContributions as E, type EventsAPI as F, type GetModelsOptions as G, type HugeIconName as H, type SchedulerAPI as I, type SchedulerJobRequest as J, type SchedulerSchedule as K, type LocalizedString as L, type ModelInfo as M, type NetworkAPI as N, type UserProfile as O, type PanelDefinition as P, type ChatAPI as Q, type ChatInstructionMessage as R, type SchedulerFirePayload as S, type ToolResult as T, type UserAPI as U, type VoiceSessionOptions as V, type ConversationPresentation as W, type LogAPI as X, type BackgroundWorkersAPI as Y, type BackgroundTaskConfig as Z, type BackgroundTaskCallback as _, type ChatOptions as a, type WeatherForecastProps as a$, type BackgroundTaskHealth as a0, type BackgroundRestartPolicy as a1, type Query as a2, type QueryOptions as a3, type StorageAPI as a4, type SecretsAPI as a5, type StorageCollectionConfig as a6, type StorageContributions as a7, type AIProvider as a8, type ModelCapabilities as a9, type SelectProps as aA, type IconPickerProps as aB, type VerticalStackProps as aC, type HorizontalStackProps as aD, type GridProps as aE, type DividerProps as aF, type IconProps as aG, type IconButtonType as aH, type IconButtonProps as aI, type PanelAction as aJ, type PanelProps as aK, type ToggleProps as aL, type CollapsibleProps as aM, type FrameVariant as aN, type FrameProps as aO, type ListProps as aP, type PillVariant as aQ, type PillProps as aR, type CheckboxProps as aS, type MarkdownProps as aT, type TextPreviewProps as aU, type ModalProps as aV, type ConditionalGroupProps as aW, type WeatherCondition as aX, type WeatherWind as aY, type WeatherNowProps as aZ, type WeatherForecastStep as a_, type ChatImage as aa, type ToolCall as ab, type VoiceTransportRequest as ac, type Tool as ad, type ToolAttachment as ae, type Action as af, type ExtensionModule as ag, type AllowedCSSProperty as ah, type ExtensionComponentStyle as ai, type ExtensionComponentData as aj, type ExtensionComponentIterator as ak, type ExtensionComponentChildren as al, type ExtensionActionCall as am, type ExtensionActionRef as an, type ExtensionDataSource as ao, type ExtensionPanelDefinition as ap, type HeaderProps as aq, type LabelProps as ar, type ClockProps as as, type ParagraphProps as at, type ButtonProps as au, type TextInputProps as av, type PasswordInputProps as aw, type NumberInputProps as ax, type TextAreaProps as ay, type DateTimeInputProps as az, type StreamEvent as b, type ChartKind as b0, type ChartSeries as b1, type ChartProps as b2, type StatTrend as b3, type StatTileProps as b4, type ProgressShape as b5, type ProgressColor as b6, type ProgressBarProps as b7, type KeyValueRow as b8, type KeyValueListProps as b9, type TimelineVariant as ba, type TimelineEntry as bb, type TimelineProps as bc, type NoteVariant as bd, type NoteProps as be, type CalendarEventStatus as bf, type CalendarEventProps as bg, type ExecutionContext as bh, type VoiceSessionDescriptor as c, type ToolSettingsViewDefinition as d, type ToolSettingsView as e, type ToolSettingsListView as f, type ToolSettingsListMapping as g, type ToolSettingsComponentView as h, type ToolSettingsActionDataSource as i, type StatusCardDefinition as j, type PanelView as k, type PanelComponentView as l, type PanelActionDataSource as m, type PanelUnknownView as n, type ProviderDefinition as o, type ProviderConfigView as p, type PromptContribution as q, resolveLocalizedString as r, type PromptSection as s, type ToolDefinition as t, type ToolConfirmationConfig as u, type CommandDefinition as v, type ExtensionContext as w, type SettingsAPI as x, type ProvidersAPI as y, type ToolsAPI as z };
2510
+ export { type BackgroundTaskConfig as $, type ActionResult as A, type ActionsAPI as B, type ChatMessage as C, type Disposable as D, type ExtensionContributions as E, type EventsAPI as F, type GetModelsOptions as G, type HugeIconName as H, type SchedulerAPI as I, type SchedulerJobRequest as J, type SchedulerSchedule as K, type LocalizedString as L, type ModelInfo as M, type NetworkAPI as N, type UserProfile as O, type PanelDefinition as P, type ChatAPI as Q, type ChatInstructionMessage as R, type SchedulerFirePayload as S, type ToolResult as T, type UserAPI as U, type VoiceSessionOptions as V, type ConversationPresentation as W, type AttachmentsAPI as X, type AttachmentContent as Y, type LogAPI as Z, type BackgroundWorkersAPI as _, type ChatOptions as a, type WeatherWind as a$, type BackgroundTaskCallback as a0, type BackgroundTaskContext as a1, type BackgroundTaskHealth as a2, type BackgroundRestartPolicy as a3, type Query as a4, type QueryOptions as a5, type StorageAPI as a6, type SecretsAPI as a7, type StorageCollectionConfig as a8, type StorageContributions as a9, type NumberInputProps as aA, type TextAreaProps as aB, type DateTimeInputProps as aC, type SelectProps as aD, type IconPickerProps as aE, type VerticalStackProps as aF, type HorizontalStackProps as aG, type GridProps as aH, type DividerProps as aI, type IconProps as aJ, type IconButtonType as aK, type IconButtonProps as aL, type PanelAction as aM, type PanelProps as aN, type ToggleProps as aO, type CollapsibleProps as aP, type FrameVariant as aQ, type FrameProps as aR, type ListProps as aS, type PillVariant as aT, type PillProps as aU, type CheckboxProps as aV, type MarkdownProps as aW, type TextPreviewProps as aX, type ModalProps as aY, type ConditionalGroupProps as aZ, type WeatherCondition as a_, type AIProvider as aa, type ModelCapabilities as ab, type ChatImage as ac, type ChatFile as ad, type ToolCall as ae, type VoiceTransportRequest as af, type Tool as ag, type ToolAttachment as ah, type Action as ai, type ExtensionModule as aj, type AllowedCSSProperty as ak, type ExtensionComponentStyle as al, type ExtensionComponentData as am, type ExtensionComponentIterator as an, type ExtensionComponentChildren as ao, type ExtensionActionCall as ap, type ExtensionActionRef as aq, type ExtensionDataSource as ar, type ExtensionPanelDefinition as as, type HeaderProps as at, type LabelProps as au, type ClockProps as av, type ParagraphProps as aw, type ButtonProps as ax, type TextInputProps as ay, type PasswordInputProps as az, type StreamEvent as b, type WeatherNowProps as b0, type WeatherForecastStep as b1, type WeatherForecastProps as b2, type ChartKind as b3, type ChartSeries as b4, type ChartProps as b5, type StatTrend as b6, type StatTileProps as b7, type ProgressShape as b8, type ProgressColor as b9, type ProgressBarProps as ba, type KeyValueRow as bb, type KeyValueListProps as bc, type TimelineVariant as bd, type TimelineEntry as be, type TimelineProps as bf, type NoteVariant as bg, type NoteProps as bh, type CalendarEventStatus as bi, type CalendarEventProps as bj, type ExecutionContext as bk, type VoiceSessionDescriptor as c, type ToolSettingsViewDefinition as d, type ToolSettingsView as e, type ToolSettingsListView as f, type ToolSettingsListMapping as g, type ToolSettingsComponentView as h, type ToolSettingsActionDataSource as i, type StatusCardDefinition as j, type PanelView as k, type PanelComponentView as l, type PanelActionDataSource as m, type PanelUnknownView as n, type ProviderDefinition as o, type ProviderConfigView as p, type PromptContribution as q, resolveLocalizedString as r, type PromptSection as s, type ToolDefinition as t, type ToolConfirmationConfig as u, type CommandDefinition as v, type ExtensionContext as w, type SettingsAPI as x, type ProvidersAPI as y, type ToolsAPI as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stina/extension-api",
3
- "version": "1.3.1",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -110,6 +110,7 @@
110
110
  "user.location.read",
111
111
  "chat.history.read",
112
112
  "chat.current.read",
113
+ "attachments.read",
113
114
  "chat.message.write",
114
115
  "provider.register",
115
116
  "tools.register",
package/src/index.ts CHANGED
@@ -63,6 +63,8 @@ export type {
63
63
  ChatAPI,
64
64
  ChatInstructionMessage,
65
65
  ConversationPresentation,
66
+ AttachmentsAPI,
67
+ AttachmentContent,
66
68
  LogAPI,
67
69
 
68
70
  // Background workers
@@ -87,6 +89,7 @@ export type {
87
89
  ModelCapabilities,
88
90
  ChatMessage,
89
91
  ChatImage,
92
+ ChatFile,
90
93
  ChatOptions,
91
94
  GetModelsOptions,
92
95
  StreamEvent,
package/src/messages.ts CHANGED
@@ -264,6 +264,8 @@ export type RequestMethod =
264
264
  // Tools cross-extension methods
265
265
  | 'tools.list'
266
266
  | 'tools.execute'
267
+ // Attachments
268
+ | 'attachments.read'
267
269
 
268
270
  export interface ProviderRegisteredMessage {
269
271
  type: 'provider-registered'
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { createExecutionContext } from './executionContext.js'
3
+ import type { ExtensionContext } from '../types.js'
4
+
5
+ const extensionContext = {
6
+ extension: { id: 'mail-reader', version: '1.0.0', storagePath: '/tmp/x' },
7
+ } as ExtensionContext
8
+
9
+ /**
10
+ * What a tool is handed when it runs.
11
+ *
12
+ * The attachments API is the part worth pinning: it is absent unless the extension
13
+ * asked for the permission *and* the request knows whose work it is, and an absent
14
+ * API is what makes a tool check before reaching for files it may not have.
15
+ */
16
+ describe('createExecutionContext', () => {
17
+ it('leaves attachments out without the permission', () => {
18
+ const context = createExecutionContext(vi.fn(), extensionContext, 'user-1', false)
19
+
20
+ expect(context.attachments).toBeUndefined()
21
+ })
22
+
23
+ it('leaves attachments out without a user, whatever the permission says', () => {
24
+ // An attachment belongs to somebody. A request that cannot say whose work it is
25
+ // doing has no business reading one.
26
+ const context = createExecutionContext(vi.fn(), extensionContext, undefined, true)
27
+
28
+ expect(context.attachments).toBeUndefined()
29
+ })
30
+
31
+ it('reads an attachment for the user the request belongs to', async () => {
32
+ const sendRequest = vi.fn().mockResolvedValue({
33
+ id: 'att-7',
34
+ mime: 'application/pdf',
35
+ data: 'JVBE',
36
+ byteSize: 4,
37
+ })
38
+
39
+ const context = createExecutionContext(sendRequest, extensionContext, 'user-1', true)
40
+ const content = await context.attachments!.read('att-7')
41
+
42
+ // The user id is the runtime's to supply, not the tool's: it comes from the
43
+ // request, so a tool cannot reach another user's file by passing a different one.
44
+ expect(sendRequest).toHaveBeenCalledWith('attachments.read', {
45
+ attachmentId: 'att-7',
46
+ userId: 'user-1',
47
+ })
48
+ expect(content?.mime).toBe('application/pdf')
49
+ })
50
+
51
+ it('passes a missing attachment through as nothing', async () => {
52
+ const context = createExecutionContext(
53
+ vi.fn().mockResolvedValue(null),
54
+ extensionContext,
55
+ 'user-1',
56
+ true
57
+ )
58
+
59
+ expect(await context.attachments!.read('gone')).toBeNull()
60
+ })
61
+
62
+ it('still builds the rest of the context without the permission', () => {
63
+ const context = createExecutionContext(vi.fn(), extensionContext, 'user-1')
64
+
65
+ expect(context.userId).toBe('user-1')
66
+ expect(context.storage).toBeDefined()
67
+ expect(context.userStorage).toBeDefined()
68
+ expect(context.secrets).toBeDefined()
69
+ expect(context.userSecrets).toBeDefined()
70
+ })
71
+ })
@@ -2,7 +2,7 @@
2
2
  * Shared execution context builder for tool, action, and scheduler operations.
3
3
  */
4
4
 
5
- import type { ExecutionContext, ExtensionContext } from '../types.js'
5
+ import type { AttachmentContent, ExecutionContext, ExtensionContext } from '../types.js'
6
6
  import type { RequestMessage } from '../messages.js'
7
7
  import { buildExtensionStorageAPI, buildUserStorageAPI } from './storageApi.js'
8
8
  import { buildExtensionSecretsAPI, buildUserSecretsAPI } from './secretsApi.js'
@@ -16,7 +16,13 @@ type SendRequest = <T>(method: RequestMessage['method'], payload: unknown) => Pr
16
16
  export function createExecutionContext(
17
17
  sendRequest: SendRequest,
18
18
  extensionContext: ExtensionContext,
19
- userId?: string
19
+ userId?: string,
20
+ /**
21
+ * Whether the extension declared `attachments.read`. Checked here rather than in
22
+ * the host alone so a tool can see whether the capability is there at all, the
23
+ * way it can for storage — the host refuses it regardless.
24
+ */
25
+ canReadAttachments = false
20
26
  ): ExecutionContext {
21
27
  return {
22
28
  userId,
@@ -33,5 +39,19 @@ export function createExecutionContext(
33
39
  userSecrets: userId
34
40
  ? buildUserSecretsAPI(sendRequest, userId)
35
41
  : buildExtensionSecretsAPI(sendRequest),
42
+ // Only with a user to scope it to. An attachment belongs to somebody, and a
43
+ // request that cannot say whose work it is doing has no business reading one.
44
+ ...(canReadAttachments && userId
45
+ ? {
46
+ attachments: {
47
+ async read(attachmentId: string): Promise<AttachmentContent | null> {
48
+ return sendRequest<AttachmentContent | null>('attachments.read', {
49
+ attachmentId,
50
+ userId,
51
+ })
52
+ },
53
+ },
54
+ }
55
+ : {}),
36
56
  }
37
57
  }
package/src/runtime.ts CHANGED
@@ -105,6 +105,13 @@ const messagePort = getMessagePort()
105
105
  let extensionModule: ExtensionModule | null = null
106
106
  let extensionDisposable: Disposable | null = null
107
107
  let extensionContext: ExtensionContext | null = null
108
+ /**
109
+ * What this extension was granted, kept from activation.
110
+ *
111
+ * A request-scoped context is built long after `handleActivate` has returned, and
112
+ * has to know the same things it did.
113
+ */
114
+ let grantedPermissions: string[] = []
108
115
  let backgroundTaskManager: WorkerBackgroundTaskManager | null = null
109
116
 
110
117
  const pendingRequests = new Map<string, PendingRequest>()
@@ -284,6 +291,7 @@ async function handleActivate(payload: {
284
291
  settings: Record<string, unknown>
285
292
  }): Promise<void> {
286
293
  const { extensionId, extensionVersion, storagePath, permissions } = payload
294
+ grantedPermissions = permissions
287
295
 
288
296
  // Build the context based on permissions
289
297
  extensionContext = buildContext(extensionId, extensionVersion, storagePath, permissions)
@@ -346,7 +354,12 @@ function handleSettingsChanged(key: string, value: unknown): void {
346
354
  // ExecutionContext builder is in runtime/executionContext.ts
347
355
 
348
356
  async function handleSchedulerFire(payload: SchedulerFirePayload): Promise<void> {
349
- const execContext = createExecutionContext(sendRequest, extensionContext!, payload.userId)
357
+ const execContext = createExecutionContext(
358
+ sendRequest,
359
+ extensionContext!,
360
+ payload.userId,
361
+ grantedPermissions.includes('attachments.read')
362
+ )
350
363
 
351
364
  // Run callbacks concurrently to avoid blocking
352
365
  const results = await Promise.allSettled(
@@ -543,7 +556,12 @@ async function handleToolExecuteRequest(
543
556
  }
544
557
 
545
558
  try {
546
- const execContext = createExecutionContext(sendRequest, extensionContext!, payload.userId)
559
+ const execContext = createExecutionContext(
560
+ sendRequest,
561
+ extensionContext!,
562
+ payload.userId,
563
+ grantedPermissions.includes('attachments.read')
564
+ )
547
565
 
548
566
  const result = await tool.execute(payload.params, execContext)
549
567
 
@@ -586,7 +604,12 @@ async function handleActionExecuteRequest(
586
604
  }
587
605
 
588
606
  try {
589
- const execContext = createExecutionContext(sendRequest, extensionContext!, payload.userId)
607
+ const execContext = createExecutionContext(
608
+ sendRequest,
609
+ extensionContext!,
610
+ payload.userId,
611
+ grantedPermissions.includes('attachments.read')
612
+ )
590
613
 
591
614
  const result = await action.execute(payload.params, execContext)
592
615
 
@@ -20,6 +20,7 @@ export const VALID_PERMISSIONS = [
20
20
  'user.location.read',
21
21
  'chat.history.read',
22
22
  'chat.current.read',
23
+ 'attachments.read',
23
24
  'chat.message.write',
24
25
  'provider.register',
25
26
  'tools.register',
@@ -75,7 +76,14 @@ const StoragePermissionSchema = z.enum(['storage.collections', 'secrets.manage']
75
76
  * User data permission schema
76
77
  */
77
78
  const UserDataPermissionSchema = z
78
- .enum(['user.profile.read', 'user.list', 'user.location.read', 'chat.history.read', 'chat.current.read'])
79
+ .enum([
80
+ 'user.profile.read',
81
+ 'user.list',
82
+ 'user.location.read',
83
+ 'chat.history.read',
84
+ 'chat.current.read',
85
+ 'attachments.read',
86
+ ])
79
87
  .describe('User data access permission')
80
88
 
81
89
  /**