@ixo/oracle-runtime-workers 0.30.0 → 0.32.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 (53) hide show
  1. package/package.json +7 -5
  2. package/src/attachments/channel-links.ts +135 -0
  3. package/src/attachments/channel.ts +203 -0
  4. package/src/attachments/download.ts +23 -6
  5. package/src/attachments/index.ts +23 -0
  6. package/src/attachments/pipeline.ts +120 -42
  7. package/src/attachments/retention.ts +11 -3
  8. package/src/attachments/sanitize.ts +20 -8
  9. package/src/attachments/types.ts +7 -0
  10. package/src/attachments/view.ts +24 -7
  11. package/src/channels/contract.ts +69 -0
  12. package/src/channels/turns.ts +23 -1
  13. package/src/core/main-agent-types.ts +2 -0
  14. package/src/core/middlewares/tool-repetition-guard.ts +6 -3
  15. package/src/core/middlewares/turn-boundary.ts +6 -1
  16. package/src/core/runtime-context.ts +3 -0
  17. package/src/core/tool-not-run.ts +30 -0
  18. package/src/core/wrap-plugin-tool.ts +9 -1
  19. package/src/delivery/plan.ts +3 -1
  20. package/src/do/contracts.ts +3 -0
  21. package/src/do/turn-attachments.ts +103 -0
  22. package/src/do/user-oracle-do.ts +220 -57
  23. package/src/hub-actions/chain-tx-refusals.ts +217 -0
  24. package/src/hub-actions/client.ts +170 -26
  25. package/src/hub-actions/hub-text.ts +82 -0
  26. package/src/hub-actions/request.ts +39 -29
  27. package/src/hub-actions/store.ts +5 -1
  28. package/src/hub-actions/transfer.ts +99 -32
  29. package/src/index.ts +1 -0
  30. package/src/plugin-api/index.ts +1 -0
  31. package/src/plugin-api/types.ts +102 -20
  32. package/src/plugins/attachments/view-attachment-tool.ts +7 -3
  33. package/src/plugins/hub-actions/chain-tx-catalog.ts +1239 -0
  34. package/src/plugins/hub-actions/hub-actions.plugin.ts +490 -232
  35. package/src/plugins/ixo-chain/blocksync.ts +6 -2
  36. package/src/plugins/ixo-chain/http.ts +16 -8
  37. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +1 -1
  38. package/src/plugins/ixo-chain/kinds/chain/index.ts +2 -0
  39. package/src/plugins/ixo-chain/kinds/chain/validators.ts +151 -0
  40. package/src/plugins/ixo-chain/rest.ts +7 -1
  41. package/src/plugins/yoma/catalog.ts +780 -0
  42. package/src/plugins/yoma/clef.ts +98 -0
  43. package/src/plugins/yoma/drafts.ts +339 -0
  44. package/src/plugins/yoma/engine.ts +978 -0
  45. package/src/plugins/yoma/index.ts +13 -0
  46. package/src/plugins/yoma/judge.ts +358 -0
  47. package/src/plugins/yoma/permission.ts +62 -0
  48. package/src/plugins/yoma/reads.ts +195 -0
  49. package/src/plugins/yoma/submission.ts +531 -0
  50. package/src/plugins/yoma/text.ts +16 -0
  51. package/src/plugins/yoma/work.ts +190 -0
  52. package/src/plugins/yoma/yoma.plugin.ts +2171 -0
  53. package/src/sqlite/serialization.ts +2 -0
@@ -42,13 +42,17 @@ const HUB_RETENTION_SECONDS = 30 * 24 * 60 * 60;
42
42
  /**
43
43
  * A refusal the model can act on: a cap reached, a request that expired, or
44
44
  * an operation the owner's own state rules out (an unknown connection, an
45
- * email whose earlier send is unconfirmed: `priorActionId`).
45
+ * email whose earlier send is unconfirmed: `priorActionId`). A refusal
46
+ * Auth Hub answered carries its HTTP status (`httpStatus`), so a caller can
47
+ * tell a 429 or 503, which pass with time, from a deterministic refusal
48
+ * without reading the words.
46
49
  */
47
50
  export class HubActionRefused extends Error {
48
51
  constructor(
49
52
  readonly kind: 'limit' | 'expired' | 'refused',
50
53
  message: string,
51
54
  readonly priorActionId?: string,
55
+ readonly httpStatus?: number,
52
56
  ) {
53
57
  super(message);
54
58
  this.name = 'HubActionRefused';
@@ -1,9 +1,12 @@
1
- import type {
2
- ActionOperationV1,
3
- ActionReceiptV1,
4
- OptionsToken,
1
+ import {
2
+ parseChainTxRefusal,
3
+ type ActionOperationV1,
4
+ type ActionReceiptV1,
5
+ type OptionsToken,
5
6
  } from '@ixo/action-protocol';
7
+ import { chainTxRefusalWords, wordableRefusal } from './chain-tx-refusals';
6
8
  import type { ChainTxMessageInput } from '../plugin-api/types';
9
+ import { quoteHubText } from './hub-text';
7
10
  import type { ChainTxHostMessage } from './request';
8
11
  import { HubActionRefused } from './store';
9
12
 
@@ -90,24 +93,67 @@ const IXO_TOKEN: TransferToken = {
90
93
  };
91
94
 
92
95
  /**
93
- * A chain transaction's bank steps in base units; the other messages pass
96
+ * A chain transaction's amounts in base units; messages without one pass
94
97
  * unchanged. Each bank step names a display `amount` of a token from the
95
98
  * owner's list (`denom`, absent = uixo): the token must be listed, the
96
99
  * amount converts with its listed decimals (`toBaseUnits`: a plain decimal,
97
100
  * no more digits after the point than the decimals, above zero; an
98
101
  * unverified token lists 0: base units), and the bank steps of one token
99
- * together stay within its listed balance. `tokens` is the owner's list
100
- * (hub_action_options `transfer.tokens`), null when the Hub listed none;
101
- * the caller reads it only when there is a bank step.
102
+ * together stay within its listed balance. Each stake step (stake, unstake,
103
+ * move stake) names a display `amount` of IXO, converted with IXO's 6
104
+ * decimals; what a stake takes from the wallet counts against the listed
105
+ * IXO balance together with the IXO bank steps, when the list has IXO
106
+ * (Auth Hub checks every balance again before the card is made). `tokens`
107
+ * is the owner's list (hub_action_options `transfer.tokens`), null when the
108
+ * Hub listed none; the caller reads it only when there is a bank or stake
109
+ * step.
102
110
  */
103
- export function bankStepsInBaseUnits(
111
+ export function chainTxAmountsInBaseUnits(
104
112
  messages: readonly ChainTxMessageInput[],
105
113
  tokens: readonly OptionsToken[] | null,
106
114
  ): ChainTxHostMessage[] {
107
115
  const spent = new Map<string, bigint>();
108
- return messages.map((message, index) => {
109
- if (message.type !== 'cosmos.bank.send') return message;
116
+ /** The bank and stake steps of one listed token together, within its listed balance. */
117
+ const spend = (
118
+ at: string,
119
+ token: OptionsToken,
120
+ amount: bigint,
121
+ what: string,
122
+ ) => {
123
+ const total = (spent.get(token.denom) ?? 0n) + amount;
124
+ spent.set(token.denom, total);
125
+ if (total > BigInt(token.balanceBaseUnits))
126
+ throw new HubActionRefused(
127
+ 'refused',
128
+ `${at}.amount: the owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than ${total === amount ? what : 'what the steps of this transaction take of it together'}.`,
129
+ );
130
+ };
131
+ const converted = (at: string, amount: string, decimals: number) => {
132
+ try {
133
+ return toBaseUnits(amount, decimals);
134
+ } catch (error) {
135
+ if (error instanceof HubActionRefused)
136
+ throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
137
+ throw error;
138
+ }
139
+ };
140
+ return messages.map((message, index): ChainTxHostMessage => {
110
141
  const at = `messages[${index}]`;
142
+ if (
143
+ message.type === 'cosmos.staking.delegate' ||
144
+ message.type === 'cosmos.staking.undelegate' ||
145
+ message.type === 'cosmos.staking.redelegate'
146
+ ) {
147
+ const { amount: display, ...rest } = message;
148
+ const amount = converted(at, display, IXO_TOKEN.decimals);
149
+ // Only a stake takes IXO from the wallet: unstaking and moving stake
150
+ // work on what is already staked.
151
+ const ixo = tokens?.find((item) => item.denom === IXO_TOKEN.denom);
152
+ if (message.type === 'cosmos.staking.delegate' && ixo)
153
+ spend(at, ixo, amount, 'this stake');
154
+ return { ...rest, amountBaseUnits: amount.toString() };
155
+ }
156
+ if (message.type !== 'cosmos.bank.send') return message;
111
157
  if (!tokens?.length)
112
158
  throw new HubActionRefused(
113
159
  'refused',
@@ -123,21 +169,8 @@ export function bankStepsInBaseUnits(
123
169
  .slice(0, 10)
124
170
  .join(', ')}).`,
125
171
  );
126
- let amount: bigint;
127
- try {
128
- amount = toBaseUnits(message.amount, token.decimals);
129
- } catch (error) {
130
- if (error instanceof HubActionRefused)
131
- throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
132
- throw error;
133
- }
134
- const total = (spent.get(token.denom) ?? 0n) + amount;
135
- spent.set(token.denom, total);
136
- if (total > BigInt(token.balanceBaseUnits))
137
- throw new HubActionRefused(
138
- 'refused',
139
- `${at}.amount: the owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than ${total === amount ? 'this send' : 'the bank steps of this token in this transaction'}.`,
140
- );
172
+ const amount = converted(at, message.amount, token.decimals);
173
+ spend(at, token, amount, 'this send');
141
174
  return {
142
175
  type: 'cosmos.bank.send',
143
176
  toAddress: message.toAddress,
@@ -279,11 +312,14 @@ function hubErrorBody(
279
312
  return null;
280
313
  const reason =
281
314
  'reason' in body && typeof body.reason === 'string' && body.reason.trim()
282
- ? body.reason.trim().slice(0, 200)
315
+ ? body.reason.trim()
283
316
  : undefined;
284
317
  return reason ? { error, reason } : { error };
285
318
  }
286
319
 
320
+ /** The longest part of a Hub refusal's `reason` the model is given. */
321
+ const HUB_REASON_MAX = 200;
322
+
287
323
  /**
288
324
  * A Hub refusal of a prepare (`{error, reason?}` with status 400, 409 or
289
325
  * 503, or any 404 or 429) as a refusal the model can relay, or null when
@@ -295,7 +331,8 @@ function hubErrorBody(
295
331
  * try again later;
296
332
  * - 400 `simulation_failed`, `fee_above_ceiling`, `insufficient_funds`,
297
333
  * `not_authorized`, `operation_not_registered`: the transfer or
298
- * transaction as asked cannot go ahead (with the Hub's reason);
334
+ * transaction as asked cannot go ahead (with the Hub's reason, quoted and
335
+ * labelled as data, see `quoteHubText`);
299
336
  * - 409 `sequence_mismatch`, 503 `transfer_unavailable`: temporary, try
300
337
  * again shortly;
301
338
  * - any other 409: the account may not use this operation (deterministic,
@@ -308,24 +345,54 @@ export function hubPrepareRefusal(
308
345
  ): HubActionRefused | null {
309
346
  const parsed = hubErrorBody(body);
310
347
  const unfixable = unfixableRefusal(status, parsed, subject);
311
- if (unfixable) return new HubActionRefused('refused', unfixable);
348
+ if (unfixable)
349
+ return new HubActionRefused('refused', unfixable, undefined, status);
312
350
  if (!parsed) return null;
313
- const detail = parsed.reason ? `: ${parsed.reason}` : '';
351
+ // A chain transaction Auth Hub would not make from what it read on chain,
352
+ // or whose simulation named one message: a fixed code, in the runtime's
353
+ // own words (the reason text is not passed on). Without a code, the
354
+ // reason is passed on quoted, as below.
355
+ const coded =
356
+ status === 400 &&
357
+ subject === 'transaction' &&
358
+ (parsed.error === 'operation_not_registered' ||
359
+ parsed.error === 'simulation_failed')
360
+ ? wordableRefusal(parseChainTxRefusal(parsed.reason))
361
+ : null;
362
+ if (coded)
363
+ return new HubActionRefused(
364
+ 'refused',
365
+ chainTxRefusalWords(coded),
366
+ undefined,
367
+ status,
368
+ );
369
+ const reason = parsed.reason
370
+ ? quoteHubText('reason', parsed.reason, HUB_REASON_MAX)
371
+ : '';
314
372
  const words =
315
373
  status === 400 && Object.hasOwn(HUB_PREPARE_REFUSALS, parsed.error)
316
374
  ? HUB_PREPARE_REFUSALS[parsed.error]
317
375
  : undefined;
318
376
  if (words)
319
- return new HubActionRefused('refused', `${words(subject)}${detail}.`);
377
+ return new HubActionRefused(
378
+ 'refused',
379
+ `${words(subject)}.${reason ? ` ${reason}.` : ''}`,
380
+ undefined,
381
+ status,
382
+ );
320
383
  if (HUB_TRANSIENT_CODES[status]?.includes(parsed.error))
321
384
  return new HubActionRefused(
322
385
  'refused',
323
386
  `Auth Hub is temporarily unavailable for this (${parsed.error}); try again shortly.`,
387
+ undefined,
388
+ status,
324
389
  );
325
390
  if (status === 409)
326
391
  return new HubActionRefused(
327
392
  'refused',
328
- `This operation is not available for this account (${parsed.error}${detail}).`,
393
+ `This operation is not available for this account (${parsed.error}).${reason ? ` ${reason}.` : ''}`,
394
+ undefined,
395
+ status,
329
396
  );
330
397
  return null;
331
398
  }
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ export * from './plugin-api';
45
45
  export * from './plugins';
46
46
  export * from './do/contracts';
47
47
  export type { OwnerStore } from './owner-store/types';
48
+ export type { ChannelAttachment } from './channels/contract';
48
49
  export { MigratingOwnerStore } from './owner-store/migrating-store';
49
50
  export {
50
51
  IxoVfsOwnerStore,
@@ -42,6 +42,7 @@ export type { UserKvLimitKind } from '../core/user-kv';
42
42
  export type {
43
43
  ActionCallEventPayload,
44
44
  AgentMiddleware,
45
+ HubActionStatusRead,
45
46
  AuthExcludedRoute,
46
47
  BrowserToolCallEventPayload,
47
48
  ChatOpenAIFields,
@@ -18,7 +18,12 @@ import type { BaseMessage } from '@langchain/core/messages';
18
18
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
19
19
  import type { UserPreferences } from '../core/state';
20
20
  import type { AttachmentViewSurface } from '../attachments/view';
21
+ import type { ChannelAttachment } from '../channels/contract';
21
22
  import type { DeliveryConfig } from '../delivery/profile';
23
+ import type {
24
+ HubOperationStatus,
25
+ HubReviewStatus,
26
+ } from '../hub-actions/client';
22
27
  import type {
23
28
  ActionOperationV1,
24
29
  ActionReferenceV1,
@@ -27,13 +32,40 @@ import type {
27
32
  ChainTxMessage,
28
33
  } from '@ixo/action-protocol';
29
34
 
35
+ /** `Omit` applied to each member of a union (plain `Omit` merges them). */
36
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
37
+ ? Omit<T, K>
38
+ : never;
39
+
40
+ /**
41
+ * The owner's own identity in chain-transaction messages, which the host
42
+ * fills and the model never names: the filer's or disputer's `agentDid`
43
+ * (claims, reservations, disputes), an evaluation's `agentDid` and
44
+ * `agentAddress`, a claim permission's `creatorDid`, an entity update's
45
+ * `controllerDid` and a new entity's `ownerDid`: always `did:ixo:<signer>`,
46
+ * or the signer for an address.
47
+ */
48
+ export type ChainTxOwnerField =
49
+ | 'agentDid'
50
+ | 'agentAddress'
51
+ | 'creatorDid'
52
+ | 'controllerDid'
53
+ | 'ownerDid';
54
+
55
+ /** The stake steps: their amount is IXO, written in display units. */
56
+ export type ChainTxStakeType =
57
+ | 'cosmos.staking.delegate'
58
+ | 'cosmos.staking.undelegate'
59
+ | 'cosmos.staking.redelegate';
60
+
30
61
  /**
31
62
  * A chain-transaction message as the model writes it: the protocol's
32
- * message without the owner's identity, which the host fills (a claim's
33
- * `agentDid`, an evaluation's `agentDid` and `agentAddress`: the signer's).
34
- * A bank step names a display `amount` of a token from the owner's list
35
- * (`denom`, absent = uixo); the host converts it to the protocol's
36
- * `amountBaseUnits` from the listed decimals.
63
+ * message without the owner's identity (`ChainTxOwnerField`), which the
64
+ * host fills. A bank step names a display `amount` of a token from the
65
+ * owner's list (`denom`, absent = uixo); a stake step a display `amount` of
66
+ * IXO. The host converts both to the protocol's `amountBaseUnits` (the
67
+ * listed decimals; 6 for IXO). Every other amount (impact tokens, a
68
+ * collection's payments) is already in the protocol's units.
37
69
  */
38
70
  export type ChainTxMessageInput =
39
71
  | {
@@ -43,11 +75,16 @@ export type ChainTxMessageInput =
43
75
  amount: string;
44
76
  denom?: string;
45
77
  }
46
- | Extract<ChainTxMessage, { type: 'ixo.token.transfer' }>
47
- | Omit<Extract<ChainTxMessage, { type: 'ixo.claims.submit' }>, 'agentDid'>
48
- | Omit<
49
- Extract<ChainTxMessage, { type: 'ixo.claims.evaluate' }>,
50
- 'agentDid' | 'agentAddress'
78
+ | (DistributiveOmit<
79
+ Extract<ChainTxMessage, { type: ChainTxStakeType }>,
80
+ 'amountBaseUnits'
81
+ > & {
82
+ /** A plain decimal in IXO ("100" or "0.5"). */
83
+ amount: string;
84
+ })
85
+ | DistributiveOmit<
86
+ Exclude<ChainTxMessage, { type: 'cosmos.bank.send' | ChainTxStakeType }>,
87
+ ChainTxOwnerField
51
88
  >;
52
89
 
53
90
  /**
@@ -551,6 +588,25 @@ export type SessionSurface =
551
588
  | { kind: 'stream' }
552
589
  | { kind: 'chat'; surface: string; label: string };
553
590
 
591
+ /** What `ctx.hubActions.status` reads. */
592
+ export interface HubActionStatusRead {
593
+ /** False when this owner object holds no action with this id (Auth Hub was not asked). */
594
+ held: boolean;
595
+ /**
596
+ * Auth Hub's review status (`prepared`, `authorized`, `declined`,
597
+ * `expired`, …) and operation status, each one the protocol lists or
598
+ * `unknown` for anything else Auth Hub answered; null when not held.
599
+ * With no outcome yet they tell an action still waiting from one whose
600
+ * review lapsed.
601
+ */
602
+ reviewStatus: HubReviewStatus | null;
603
+ operationStatus: HubOperationStatus | null;
604
+ /** The latest authenticated outcome; null when there is none (yet). */
605
+ outcome: ActionOutcomeV1 | null;
606
+ /** For a bank-send receipt, the owner-facing line. */
607
+ summary: string | null;
608
+ }
609
+
554
610
  export interface RuntimeContext<TConfig = MergedConfig> {
555
611
  /** Authenticated user identity (validated by core auth middleware). */
556
612
  user: {
@@ -575,6 +631,14 @@ export interface RuntimeContext<TConfig = MergedConfig> {
575
631
  provider: string;
576
632
  bindingId: string;
577
633
  bindingRevision: number;
634
+ /**
635
+ * Files the person sent with this turn's message (a WhatsApp photo,
636
+ * video, document or audio file), as the channel gateway serves them:
637
+ * a capability `url` plus the `sha256` of its bytes. Absent when the
638
+ * message carried none. The model also sees them through the
639
+ * attachment pipeline; this is the raw list for tools that keep them.
640
+ */
641
+ attachments?: readonly ChannelAttachment[];
578
642
  };
579
643
  roomId?: string;
580
644
  /**
@@ -780,7 +844,13 @@ export interface RuntimeContext<TConfig = MergedConfig> {
780
844
 
781
845
  /** Host-owned action coordinator. It accepts only runtime-derived channel context. */
782
846
  hubActions?: {
783
- capabilities(): Promise<ActionCapabilitiesV1>;
847
+ /**
848
+ * The Hub's capabilities for this oracle account (cached 60 s).
849
+ * `turn`: a read on a turn's critical path (a tool list): a short
850
+ * timeout, no new read within 30 s of a failed one, and the last
851
+ * capabilities read while reads fail.
852
+ */
853
+ capabilities(opts?: { turn?: boolean }): Promise<ActionCapabilitiesV1>;
784
854
  options(input: {
785
855
  ownerDid: string;
786
856
  provider: string;
@@ -790,13 +860,16 @@ export interface RuntimeContext<TConfig = MergedConfig> {
790
860
  /**
791
861
  * One chain transaction (`ixo.chain.tx.v1`) of 1–8 messages: the host
792
862
  * adds the chain id and network (the account's), the signer (the
793
- * owner's address) and the owner's identity in claims messages
794
- * (`agentDid` = `did:ixo:<signer>`, an evaluation's `agentAddress` = the
795
- * signer) and prepares it like any operation (no fee: the Hub simulates
796
- * gas and sets it). A bank step's display `amount` is converted to base
797
- * units from the owner's token list (one options read per call, only
798
- * when there is a bank step), refused for a token not listed, more
799
- * digits than its decimals, zero or more than the listed balance.
863
+ * owner's address) and the owner's identity (`ChainTxOwnerField`:
864
+ * `did:ixo:<signer>`, an evaluation's `agentAddress` = the signer) and
865
+ * prepares it like any operation (no fee: the Hub simulates gas and
866
+ * sets it). A bank step's display `amount` is converted to base units
867
+ * from the owner's token list (one options read per call, only when
868
+ * there is a bank or stake step), refused for a token not listed, more
869
+ * digits than its decimals, zero or more than the listed balance. A
870
+ * stake step's IXO `amount` is converted with 6 decimals; what a stake
871
+ * takes from the wallet counts against the listed IXO balance with the
872
+ * IXO bank steps, when the list has IXO.
800
873
  */
801
874
  chainTransaction(input: {
802
875
  ownerDid: string;
@@ -821,11 +894,20 @@ export interface RuntimeContext<TConfig = MergedConfig> {
821
894
  requestKey: string;
822
895
  operation: ActionOperationV1;
823
896
  }): Promise<ActionReferenceV1>;
824
- /** The latest authenticated outcome and, for a transfer, its owner-facing line. */
897
+ /**
898
+ * A fresh, authenticated read of one of this owner's actions: Auth
899
+ * Hub's review and operation status, the latest outcome (null: none
900
+ * yet) and, for a transfer, its owner-facing line. An action this
901
+ * owner object never prepared is `held: false` and Auth Hub is not
902
+ * asked. `timeoutMs` bounds the Hub read (default 10 s). Rejects with
903
+ * `HubActionRefused` (with `httpStatus`) when Auth Hub refuses the read,
904
+ * for example when it must be updated first.
905
+ */
825
906
  status(
826
907
  ownerDid: string,
827
908
  actionId: string,
828
- ): Promise<{ outcome: ActionOutcomeV1 | null; summary: string | null }>;
909
+ opts?: { timeoutMs?: number },
910
+ ): Promise<HubActionStatusRead>;
829
911
  };
830
912
 
831
913
  decisions: {
@@ -20,6 +20,9 @@ import { z } from 'zod';
20
20
  import {
21
21
  ATTACHMENT_VIEW_SOURCE,
22
22
  buildUserMessageContent,
23
+ NOTE_FILENAME_MAX,
24
+ NOTE_MIMETYPE_MAX,
25
+ sanitizeNoteField,
23
26
  VIEW_ATTACHMENT_TOOL_NAME,
24
27
  } from '../../attachments';
25
28
  import { tool } from '../../plugin-api/tool-helper';
@@ -30,7 +33,7 @@ export const viewAttachmentSchema = z.object({
30
33
  .string()
31
34
  .min(1)
32
35
  .describe(
33
- 'The attachment reference exactly as listed in the "[attachment offloaded]" note of the message it belongs to (a Matrix event id such as "$abc…" or an mxc:// URI).',
36
+ 'The attachment reference exactly as listed in the "[attachment offloaded]" note of the message it belongs to (a Matrix event id such as "$abc…", a channel file reference such as "channel:0123456789abcdef", or an mxc:// URI).',
34
37
  ),
35
38
  });
36
39
 
@@ -52,12 +55,13 @@ export function createViewAttachmentTool(): PluginTool {
52
55
  }
53
56
  const { meta, view } = await ctx.attachments.view(ref);
54
57
  if (view.kind === 'text') return view.text;
58
+ const name = sanitizeNoteField(meta.filename, NOTE_FILENAME_MAX);
55
59
  if (!ctx.toolCallId) {
56
60
  throw new Error(
57
- `"${meta.filename}" was fetched but cannot be re-attached outside a tool call`,
61
+ `"${name}" was fetched but cannot be re-attached outside a tool call`,
58
62
  );
59
63
  }
60
- const label = `Re-attached "${meta.filename}" (${meta.mimetype}) from earlier in this conversation.`;
64
+ const label = `Re-attached "${name}" (${sanitizeNoteField(meta.mimetype, NOTE_MIMETYPE_MAX)}) from earlier in this conversation.`;
61
65
  const timestamp = new Date().toISOString();
62
66
  const msgFromMatrixRoom = ctx.session.client === 'matrix';
63
67
  return new Command({