@ixo/oracle-runtime-workers 0.37.2 → 0.39.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 (111) hide show
  1. package/package.json +4 -4
  2. package/src/attachments/channel-links.ts +74 -7
  3. package/src/attachments/channel.ts +15 -4
  4. package/src/attachments/content-blocks.ts +19 -16
  5. package/src/attachments/download.ts +173 -50
  6. package/src/attachments/index.ts +6 -5
  7. package/src/attachments/pipeline.ts +117 -49
  8. package/src/attachments/resize.ts +142 -0
  9. package/src/attachments/retention.ts +26 -70
  10. package/src/attachments/room-media.ts +643 -0
  11. package/src/attachments/route.ts +47 -4
  12. package/src/attachments/sandbox-archive.ts +15 -4
  13. package/src/attachments/sanitize.ts +24 -13
  14. package/src/attachments/view.ts +82 -38
  15. package/src/channels/replies.ts +8 -4
  16. package/src/core/capability-expiry.ts +62 -0
  17. package/src/core/capability-router.ts +126 -95
  18. package/src/core/context-budget.ts +116 -4
  19. package/src/core/domain-context/document.ts +74 -2
  20. package/src/core/domain-context/index.ts +1 -0
  21. package/src/core/domain-context/resolver.ts +9 -6
  22. package/src/core/domain-context/turn-context.ts +62 -13
  23. package/src/core/domain-context/types.ts +17 -0
  24. package/src/core/env.ts +21 -2
  25. package/src/core/index.ts +4 -0
  26. package/src/core/llm.ts +29 -0
  27. package/src/core/main-agent-types.ts +53 -4
  28. package/src/core/main-agent.ts +228 -125
  29. package/src/core/manifest.ts +109 -24
  30. package/src/core/mcp-result.ts +56 -0
  31. package/src/core/meta-tools.ts +258 -110
  32. package/src/core/middlewares/attachment-recall.ts +502 -0
  33. package/src/core/middlewares/capability-gate.ts +77 -28
  34. package/src/core/middlewares/context-guard.ts +154 -21
  35. package/src/core/middlewares/index.ts +5 -0
  36. package/src/core/middlewares/result-cap.ts +1 -1
  37. package/src/core/middlewares/summarization.ts +9 -0
  38. package/src/core/middlewares/tool-marks.ts +11 -2
  39. package/src/core/middlewares/turn-boundary.ts +22 -12
  40. package/src/core/operating-guides.ts +126 -0
  41. package/src/core/plugins/skills/skills-tools.ts +6 -31
  42. package/src/core/plugins/skills/skills.plugin.ts +4 -3
  43. package/src/core/plugins/weather/weather.plugin.ts +1 -0
  44. package/src/core/prompt-composer.ts +188 -111
  45. package/src/core/read-result-tool.ts +48 -14
  46. package/src/core/registries.ts +13 -5
  47. package/src/core/state.ts +4 -3
  48. package/src/core/sub-agent-fallback.ts +1 -0
  49. package/src/core/subagent-as-tool.ts +4 -1
  50. package/src/core/tool-access.ts +3 -32
  51. package/src/core/wrap-plugin-tool.ts +2 -1
  52. package/src/do/contracts.ts +64 -24
  53. package/src/do/room-mirror.ts +144 -20
  54. package/src/do/transcript.ts +41 -12
  55. package/src/do/turn-attachments.ts +45 -6
  56. package/src/do/turn-metadata.ts +16 -4
  57. package/src/do/user-oracle-do.ts +639 -152
  58. package/src/index.ts +1 -0
  59. package/src/llm/provider-error.ts +5 -2
  60. package/src/llm/stream-liveness.ts +3 -2
  61. package/src/matrix/gateway-do.ts +164 -102
  62. package/src/matrix/ingest.ts +12 -3
  63. package/src/matrix/media-deadline.ts +5 -47
  64. package/src/memory/session-history-indexer.ts +60 -14
  65. package/src/plugin-api/oracle-plugin.ts +4 -5
  66. package/src/plugin-api/types.ts +32 -3
  67. package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
  68. package/src/plugins/agui/agui.plugin.ts +1 -0
  69. package/src/plugins/attachments/attachments.plugin.ts +3 -3
  70. package/src/plugins/attachments/view-attachment-tool.ts +6 -4
  71. package/src/plugins/compact-schema.ts +80 -0
  72. package/src/plugins/composio/composio.plugin.ts +1 -0
  73. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
  74. package/src/plugins/editor/editor.plugin.ts +4 -0
  75. package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
  76. package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
  77. package/src/plugins/flows/flows.plugin.ts +37 -6
  78. package/src/plugins/flows/prompts.ts +1 -1
  79. package/src/plugins/flows/tools/authoring.ts +5 -7
  80. package/src/plugins/flows/tools/discovery.ts +1 -4
  81. package/src/plugins/flows/tools/forms.ts +2 -8
  82. package/src/plugins/flows/tools/linkage.ts +1 -4
  83. package/src/plugins/flows/tools/settings.ts +1 -4
  84. package/src/plugins/flows/translator.ts +16 -5
  85. package/src/plugins/flows/types.ts +27 -48
  86. package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
  87. package/src/plugins/index.ts +2 -2
  88. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
  89. package/src/plugins/ixo-chain/kinds/core.ts +2 -2
  90. package/src/plugins/ixo-chain/kinds/types.ts +34 -5
  91. package/src/plugins/ixo-chain/queries.ts +10 -1
  92. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
  93. package/src/plugins/matrix-group-chats/index.ts +1 -0
  94. package/src/plugins/memory/memory-tools.ts +4 -4
  95. package/src/plugins/memory/memory.plugin.ts +27 -1
  96. package/src/plugins/memory/oracle-document-tools.ts +24 -49
  97. package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
  98. package/src/plugins/portal/portal.plugin.ts +1 -0
  99. package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
  100. package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
  101. package/src/plugins/tasks/manifest.ts +1 -0
  102. package/src/plugins/tasks/tasks-tools.ts +14 -19
  103. package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
  104. package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
  105. package/src/plugins/vfs/vfs-tools.ts +6 -6
  106. package/src/plugins/vfs/vfs.plugin.ts +4 -5
  107. package/src/plugins/yoma/yoma.plugin.ts +1 -0
  108. package/src/sqlite/inline-payloads.ts +121 -0
  109. package/src/sqlite/serialization.ts +12 -0
  110. package/src/sqlite/sqlite-saver.ts +258 -42
  111. package/src/tasks/schedule-input.ts +5 -22
@@ -32,70 +32,59 @@ const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/;
32
32
  const spaceSchema = z
33
33
  .enum(['public', 'private'])
34
34
  .describe(
35
- "Which knowledge space: 'public' is shared with every user of this oracle; 'private' is visible only to the domain's members (its controllers, owner, admins and DAO members).",
35
+ "'public': every user of this oracle. 'private': the domain's members only (controllers, owner, admins, DAO members).",
36
36
  );
37
37
 
38
38
  const sourceSchema = z
39
39
  .object({
40
40
  kind: z
41
41
  .enum(['attachment', 'vfs'])
42
- .describe(
43
- "'attachment' for a file the person attached in this conversation, 'vfs' for a file in their Library.",
44
- ),
42
+ .describe("'attachment': attached here. 'vfs': in their Library."),
45
43
  ref: z
46
44
  .string()
47
45
  .min(1)
48
46
  .describe(
49
- 'For kind attachment: the value inside the `[source: eventId="…"]` (or `[source: url="…"]`) line that accompanies the attached file, passed exactly as given (an event id such as "$abc…" or an mxc:// URL). For kind vfs: the file path in the Library, for example /private/policies/travel.pdf.',
47
+ 'attachment: the value in the `[source: eventId="…"]` or `[source: url="…"]` line beside the file, exactly as given ("$abc…" or an mxc:// URL). vfs: the Library path, e.g. /private/policies/travel.pdf.',
50
48
  ),
51
49
  })
52
- .strict()
53
- .describe('Where the file is.');
50
+ .strict();
54
51
 
55
52
  export const addOracleDocumentSchema = z
56
53
  .object({
57
54
  source: sourceSchema,
58
- title: z
59
- .string()
60
- .min(1)
61
- .max(200)
62
- .describe('A short human title for the document, 1 to 200 characters.'),
55
+ title: z.string().min(1).max(200),
63
56
  knowledge_space_type: spaceSchema,
64
- group_as_document: z
65
- .boolean()
66
- .describe(
67
- 'Required, no default. true: the file is ONE coherent record and is kept together under a document id (it can be listed and removed as one). false: the file is a collection of unrelated facts and each part is stored as ordinary loose knowledge with no document grouping. Decide with the person, see the tool description.',
68
- ),
57
+ group_as_document: z.boolean(),
69
58
  document_id: z
70
59
  .string()
71
60
  .regex(DOCUMENT_ID_PATTERN)
72
61
  .optional()
73
62
  .describe(
74
- "Only with group_as_document true (ignored otherwise). A stable id such as 'board-resolution-2026-10' (letters, digits, '.', '_' and '-', up to 120 characters, starting with a letter or digit). Adding a document with an id that already exists replaces it. Leave out and the tool derives one from the title and the content, so adding the same file again replaces it.",
63
+ "Grouped only: a stable id such as 'board-resolution-2026-10'. Default: derived from title and content, so adding the same file again replaces it.",
75
64
  ),
76
65
  source_description: z
77
66
  .string()
78
67
  .max(500)
79
68
  .optional()
80
69
  .describe(
81
- "Only with group_as_document true (ignored otherwise). One line saying where the document comes from, up to 500 characters (for example 'Signed PDF from the board secretary, 12 Oct 2026').",
70
+ "Grouped only: where it comes from, e.g. 'Signed PDF from the board secretary, 12 Oct 2026'.",
82
71
  ),
83
72
  if_exists: z
84
73
  .enum(['replace', 'fail'])
85
74
  .optional()
86
75
  .describe(
87
- "Only with group_as_document true (ignored otherwise). 'replace' (default) overwrites a grouped document with the same document_id; 'fail' refuses instead.",
76
+ "Grouped only: for a document_id that exists, 'replace' (default) or 'fail'.",
88
77
  ),
89
78
  allow_truncation: z
90
79
  .boolean()
91
80
  .optional()
92
81
  .describe(
93
- 'Default false. When the file is larger than the limits the tool refuses and tells you the size. Set true only after the person agreed that the first part of the file is enough.',
82
+ 'true keeps only the first part of a file over the limits. Only after the person agreed.',
94
83
  ),
95
84
  confirmed_insertion_from_user: z
96
85
  .boolean()
97
86
  .describe(
98
- 'true only when the person has explicitly asked you to store this file as oracle knowledge after you told them who will be able to see it. Otherwise ask first.',
87
+ 'true only after the person asked to store this file and you told them who will see it.',
99
88
  ),
100
89
  })
101
90
  .strict();
@@ -108,57 +97,43 @@ export const listOracleDocumentsSchema = z
108
97
  knowledge_space_type: spaceSchema,
109
98
  status: z
110
99
  .enum(['ingesting', 'ready', 'partial', 'deleting', 'delete_failed'])
111
- .optional()
112
- .describe('Only list documents in this status. Leave out for all.'),
100
+ .optional(),
113
101
  limit: z
114
102
  .number()
115
103
  .int()
116
104
  .min(1)
117
105
  .max(MAX_LIST_LIMIT)
118
106
  .optional()
119
- .describe(
120
- `Most documents to list, 1 to ${MAX_LIST_LIMIT} (default ${DEFAULT_LIST_LIMIT}).`,
121
- ),
107
+ .describe(`Default ${DEFAULT_LIST_LIMIT}.`),
122
108
  })
123
109
  .strict();
124
110
 
125
111
  export const deleteOracleDocumentSchema = z
126
112
  .object({
127
- document_id: z
128
- .string()
129
- .regex(DOCUMENT_ID_PATTERN)
130
- .describe(
131
- 'The id of a grouped document, as shown by list_oracle_documents.',
132
- ),
113
+ document_id: z.string().regex(DOCUMENT_ID_PATTERN),
133
114
  knowledge_space_type: spaceSchema,
134
- confirmed_deletion_from_user: z
135
- .boolean()
136
- .describe(
137
- 'true only when the person has explicitly asked you to remove this document after you told them it will be deleted for everyone who can see that space.',
138
- ),
115
+ confirmed_deletion_from_user: z.boolean(),
139
116
  })
140
117
  .strict();
141
118
 
142
119
  export const ADD_ORACLE_DOCUMENT_DESCRIPTION = [
143
- "Use this, not memory-engine__add_oracle_knowledge, whenever the knowledge comes from a file: one the person attached or one in their Library, even when the file's text is visible in the conversation. Store that file as this oracle's domain knowledge. For an attachment, set source to kind 'attachment' and ref to the value inside the `[source: eventId=…]` (or `[source: url=…]`) line that accompanies the attached file: pass that eventId or mxc URL as ref. For a Library file, kind 'vfs' and ref the path. The text is read out of the file, split into parts and written to the public or private knowledge space of this oracle. Only a controller, owner, admin or DAO member of the domain can do this; the memory engine checks and refuses anyone else.",
144
- '',
145
- 'Decide with group_as_document, which is required and has no default:',
146
- '- true: the file is ONE coherent record that belongs together, such as a board resolution, a contract or a report. It is stored under one document id, so it can be listed with list_oracle_documents, replaced by adding it again with the same document_id, and removed as one with delete_oracle_document.',
147
- '- false: the file is a place where someone dumped many unrelated company facts to upload them easily (for example a notes file with the office address, the refund rule, the CEO name and the opening hours). Each part is stored as an ordinary knowledge fact with no document grouping. Loose facts cannot be listed as a document or removed as a group; they are found by search and removed one by one with delete_episode.',
120
+ "Use this, not memory-engine__add_oracle_knowledge, whenever the knowledge comes from a file the person attached or one in their Library, even when its text is visible in the conversation. The file's text is split into parts and stored in this oracle's public or private knowledge space. The memory engine refuses anyone but a controller, owner, admin or DAO member of the domain.",
148
121
  '',
149
- 'If it is not obvious which one the file is, ask the person before you call this tool. Say what you found in the file in one sentence and offer the two options in plain words. Never pick one silently when the file could be either.',
122
+ 'group_as_document is required and has no default:',
123
+ '- true: ONE coherent record, such as a board resolution, a contract or a report. Kept under one document id: listed with list_oracle_documents, replaced by adding it again, removed as one with delete_oracle_document.',
124
+ '- false: a dump of unrelated company facts (a notes file with the office address, the refund rule, the opening hours). Each part becomes a loose fact: found by search, removed one by one with delete_episode.',
125
+ 'If the file could be either, ask the person first: say what you found in one sentence and offer the two options in plain words. Never pick silently.',
150
126
  '',
151
- 'Always confirm first: tell the person whether the public space (every user of this oracle) or the private space (the members of the domain only: controllers, owner, admins and DAO members) will hold the content, and set confirmed_insertion_from_user to true only after they say yes. Do not store files the person did not ask you to store.',
127
+ 'Always confirm first: tell the person which space will hold the content and only then set confirmed_insertion_from_user. Never store a file they did not ask you to store.',
152
128
  '',
153
- 'Limits: a text file or a PDF with a text layer up to 25 MB; a scanned PDF, an image or an Office file (read by a helper model) up to 10 MB. Text is split into parts of about 12 KB, at most 64 parts (about 750 KB of text, the memory engine accepts 900 KB per document). A larger file is refused with its size unless allow_truncation is true, which keeps only the first part of the file; use that only if the person agrees. Without a document_id a grouped document gets one derived from the title and content, so repeating the call after a timeout replaces rather than duplicates; the result reports the id.',
154
- 'The result says how many parts were queued. Indexing continues in the background; for a grouped document list_oracle_documents shows the status (ingesting, ready, partial, deleting or delete_failed).',
129
+ 'Limits: text or a PDF with a text layer up to 25 MB; a scanned PDF, image or Office file up to 10 MB; at most 64 parts of about 12 KB. A larger file is refused with its size unless allow_truncation is set. The result gives the parts queued and the document id; indexing continues in the background (list_oracle_documents shows its status).',
155
130
  ].join('\n');
156
131
 
157
132
  export const LIST_ORACLE_DOCUMENTS_DESCRIPTION =
158
- "List the grouped documents in one of this oracle's knowledge spaces with their id, title, status (ingesting, ready, partial, deleting or delete_failed) and number of parts. Optional status filter and limit (default 100, at most 500). Loose facts stored with group_as_document false are not listed here; they are ordinary knowledge found by search.";
133
+ "List the grouped documents in one of this oracle's knowledge spaces: id, title, status, parts. This is the oracle's knowledge base, not the domain documents of the verified domain context. Loose facts (group_as_document false) are not listed; they are found by search.";
159
134
 
160
135
  export const DELETE_ORACLE_DOCUMENT_DESCRIPTION =
161
- 'Remove a grouped document and every one of its parts from the public or private knowledge space of this oracle. Use the id from list_oracle_documents. This cannot be undone and it takes effect for everyone who can see that space, so tell the person which document will go and set confirmed_deletion_from_user to true only after they say yes. Loose facts are not documents; remove those with delete_episode. The removal runs in the background; if list_oracle_documents later shows the document as delete_failed, call this again to retry it.';
136
+ 'Remove a grouped document, with all its parts, for everyone who can see that space. Cannot be undone: tell the person which document will go and set confirmed_deletion_from_user only after they say yes. Loose facts: use delete_episode. Runs in the background; if list_oracle_documents later shows delete_failed, call this again to retry.';
162
137
 
163
138
  const NO_ENGINE_MESSAGE =
164
139
  'Memory engine is not reachable from this oracle (missing MEMORY_ENGINE_URL or signing key).';
@@ -58,6 +58,7 @@ const manifest: PluginManifest = {
58
58
  tags: ['pod', 'domain', 'design', 'orchestration', 'on-chain'],
59
59
  category: 'automation',
60
60
  visibility: 'on-demand',
61
+ brief: 'design and create an IXO POD end to end',
61
62
  stability: 'experimental',
62
63
  };
63
64
 
@@ -53,6 +53,7 @@ const manifest: PluginManifest = {
53
53
  tags: ['portal', 'browser', 'ui'],
54
54
  category: 'ui',
55
55
  visibility: 'on-demand',
56
+ brief: "act in the user's Portal through browser tools",
56
57
  stability: 'stable',
57
58
  };
58
59
 
@@ -7,25 +7,13 @@ import type { SandboxMcpTool } from './sandbox.plugin';
7
7
  const sandboxWriteBlobSchema = z.object({
8
8
  blobId: z
9
9
  .string()
10
- .describe(
11
- 'The blobId returned by the tool that produced the value (e.g. mint_invocation). Format: `blob_<16 hex chars>`.',
12
- ),
10
+ .describe('`blob_<16 hex chars>`, from the tool that made the value.'),
13
11
  path: z
14
12
  .string()
15
- .describe(
16
- 'Destination path in the sandbox, e.g. `/workspace/data/<skill>/ucan_token`. Must be under /workspace/data/.',
17
- ),
13
+ .describe('Under /workspace/data/, e.g. `/workspace/data/<skill>/token`.'),
18
14
  });
19
15
 
20
- const SANDBOX_WRITE_BLOB_DESCRIPTION = `Write a server-stored blob to a file in the sandbox without ever paste-relaying the value through the LLM.
21
-
22
- Use this whenever a tool returns a \`blobId\` (e.g. \`mint_invocation\`'s response). Pass the blobId + the destination path; the runtime looks the value up server-side and calls sandbox_write_file with its content. The long opaque value (base64 CARs, JWTs, etc.) never enters the LLM context — eliminates string-corruption-in-relay as a failure mode.
23
-
24
- Inputs:
25
- - blobId: short hex identifier returned by the producing tool (format \`blob_<16 hex chars>\`).
26
- - path: destination path in the sandbox. Must be under \`/workspace/data/\` (the sandbox enforces this; writes elsewhere are rejected).
27
-
28
- Returns: \`{ success: true, path, bytesWritten }\` on success, \`{ success: false, error }\` on failure. Common errors: blob not found (re-mint and retry — for invocations this can happen on a multi-replica deployment if the lookup lands on a different process), invalid blobId format, path outside /workspace/data/.`;
16
+ const SANDBOX_WRITE_BLOB_DESCRIPTION = `Write a server-stored value to a sandbox file without passing it through you, so a long opaque value (base64 CAR, JWT) cannot be corrupted. Use it whenever a tool returns a \`blobId\` (e.g. \`mint_invocation\`). Returns \`{ success, path, bytesWritten }\` or \`{ success: false, error }\`; on "blob not found", mint it again and retry.`;
29
17
 
30
18
  export interface CreateSandboxWriteBlobToolParams {
31
19
  /** Upstream MCP `sandbox_write_file` tool — looked up once per request from
@@ -51,10 +51,20 @@ const siblingEnvSchema = z.object({
51
51
  SKILLS_CAPSULES_BASE_URL: z.string().url().optional(),
52
52
  });
53
53
 
54
+ // In the guide and not in the summary: an `always` host would pay for it in
55
+ // the capability block on top of the tool descriptions, and a preload delivers
56
+ // a guide but not the manifest.
57
+ const OPERATING_GUIDE = [
58
+ '### Sandbox',
59
+ '- `sandbox_run` writes anywhere through the shell, `/tmp` included. `sandbox_write_file` writes raw bytes under `/workspace/data/` only: paths like `/tmp/...` or `/workspace/tmp/...` are rejected.',
60
+ "- A file at a path the user names belongs in their library (`vfs`), not here. Report the path you really wrote: `/workspace/data/...` is the box, not the user's `/...`.",
61
+ '- The box is a scratch disk, not your file store. Files under `/workspace/data/` are pushed up to durable storage after each call, but nothing comes back down automatically and the box is wiped after a few idle minutes. Any file not created in the current call must be fetched with `load_artifact` before you can read it.',
62
+ ].join('\n');
63
+
54
64
  const manifest: PluginManifest = {
55
65
  title: 'Sandbox',
56
66
  summary:
57
- 'Per-user Linux box for code execution. `sandbox_run` runs shell/python (writes anywhere via shell — incl. `/tmp` for scratch). `sandbox_write_file` writes raw bytes BUT only under `/workspace/data/` — paths like `/tmp/...` or `/workspace/tmp/...` are rejected; use `sandbox_run` for those. STORAGE: the box is a scratch disk, not your file store. Files under `/workspace/data/` are pushed UP to durable storage after each call, but NOTHING comes back DOWN automatically and the box is wiped after a few idle minutes. Any file not created in the current call must be fetched with `load_artifact` before you can read it.',
67
+ 'Per-user Linux box: `sandbox_run` runs shell or python, `sandbox_write_file` writes under `/workspace/data/`.',
58
68
  whenToUse: [
59
69
  "Execute a skill — call `sandbox_run` with `cid` so user + oracle secrets are injected; the skill folder mounts read-only at `/workspace/skills/<skill-name>/` (use the absolute paths from `load_skill`'s `skillFiles`).",
60
70
  'Read a skill file (SKILL.md, scripts, configs) — call `sandbox_run` with `cid` and shell: `cat /workspace/skills/<skill-name>/SKILL.md`, `ls /workspace/skills/<skill-name>/`, `grep -r "<pattern>" /workspace/skills/<skill-name>/`, or `sed -n "1,80p" <file>` for a line range. There is no dedicated `read_skill` tool.',
@@ -67,6 +77,7 @@ const manifest: PluginManifest = {
67
77
  'Always check the result envelope: `success === true` AND `exitCode === 0` before trusting `output`. On failure, READ the `error` text and change your approach — do not retry the same call with the same args.',
68
78
  ],
69
79
  whenNotToUse: [
80
+ 'Saving a file at a path the user names: that is their library, use `vfs`.',
70
81
  'The value is already inline in chat — just use it; opening the sandbox to echo it back wastes a turn.',
71
82
  'Fetching a URL the user just mentioned — prefer `process_file`, which archives it as an artifact (pull it into the box with `load_artifact` when you need to read it).',
72
83
  'A long human-readable page (blog, article, news) — use the Firecrawl agent.',
@@ -139,7 +150,12 @@ const manifest: PluginManifest = {
139
150
  },
140
151
  },
141
152
  ],
142
- visibility: 'always',
153
+ // Every tool loads together: running code, its files and artifacts and the
154
+ // skill lifecycle (`load_skill` included) depend on one another. A host
155
+ // that wants it on every turn sets `visibility: 'always'` through
156
+ // `manifestOverrides`.
157
+ visibility: 'on-demand',
158
+ brief: 'run code; run or publish skills; generate charts and reports',
143
159
  stability: 'stable',
144
160
  category: 'core',
145
161
  tags: ['sandbox', 'execution', 'workspace', 'artifacts'],
@@ -249,6 +265,7 @@ export class SandboxPlugin extends OraclePlugin {
249
265
  readonly name = SandboxPlugin.NAME;
250
266
  readonly version = '1.0.0';
251
267
  readonly manifest = manifest;
268
+ override readonly operatingGuide = OPERATING_GUIDE;
252
269
  override readonly configSchema = configSchema;
253
270
  override readonly autoDetectHint = 'SANDBOX_MCP_URL';
254
271
 
@@ -76,6 +76,7 @@ export const tasksManifest: PluginManifest = {
76
76
  ],
77
77
  category: 'automation',
78
78
  visibility: 'on-demand',
79
+ brief: 'reminders, scheduled and recurring background jobs',
79
80
  stability: 'beta',
80
81
  tags: ['scheduler', 'cron', 'automation', 'approval', 'background'],
81
82
  };
@@ -9,6 +9,7 @@
9
9
  * model always gets an actionable message.
10
10
  */
11
11
  import { z } from 'zod';
12
+ import { modelFacingSchema } from '../compact-schema';
12
13
  import { tool } from '../../plugin-api/tool-helper';
13
14
  import type {
14
15
  OracleTaskRecord,
@@ -32,14 +33,11 @@ const taskIdSchema = z.object({
32
33
  });
33
34
 
34
35
  const APPROVAL_FIELD_DESCRIPTION =
35
- "How each run behaves. 'never' (default) just does the work and delivers " +
36
- "the result. 'before-action' is for runs the user should sign off on first " +
37
- '(a tweet, a message, a ticket, an email, a publish): when such a task ' +
38
- 'fires it does NOT execute — it posts an approval request into the ' +
39
- "user's oracle room and waits. The user approves or declines by replying " +
40
- 'there; record the decision with `resolve_task_approval` — approval ' +
41
- 'starts the run in the background (its result is posted to the room when ' +
42
- 'it finishes), a decline drops it.';
36
+ "'never' (default): do the work and deliver the result. 'before-action': for " +
37
+ 'runs the user should sign off on first (a tweet, a message, a ticket, an ' +
38
+ 'email, a publish): each fire posts an approval request into their oracle ' +
39
+ 'room and waits; they reply there and you record it with ' +
40
+ '`resolve_task_approval`.';
43
41
 
44
42
  const taskInputShape = {
45
43
  title: z.string().min(1).max(120),
@@ -47,7 +45,7 @@ const taskInputShape = {
47
45
  .string()
48
46
  .min(1)
49
47
  .describe(
50
- 'Markdown instructions the run executes. Scheduled runs are BACKGROUND sessions with NO memory of this conversation — put every ID, URL, and name the run needs into this text, never assume the run "knows" anything discussed here.',
48
+ 'Markdown instructions for a background run with NO memory of this conversation: every ID, URL and name it needs goes here.',
51
49
  ),
52
50
  // Flat at the tool boundary (a `kind` enum + optional per-kind fields),
53
51
  // converted to the store's discriminated union on parse — see
@@ -61,11 +59,8 @@ const taskInputShape = {
61
59
  .enum(['auto', 'yes', 'no'])
62
60
  .default('auto')
63
61
  .describe(
64
- "Whether the task gets its own '[Task] <title>' room where each run's result " +
65
- "(and, for 'before-action' tasks, each approval request) is posted. 'auto' " +
66
- 'creates one for frequent (sub-daily) cron tasks, long intents and ongoing ' +
67
- 'monitor/watch/track tasks; otherwise results go to the main oracle room. ' +
68
- 'Pass yes/no only when the user asked for it.',
62
+ "Whether results and approval requests get their own '[Task] <title>' room " +
63
+ "instead of the main oracle room. 'auto' decides; yes/no only when the user asked.",
69
64
  ),
70
65
  };
71
66
 
@@ -160,7 +155,7 @@ function previewTask(): PluginTool {
160
155
  name: 'preview_task',
161
156
  effect: 'read',
162
157
  description:
163
- 'Validate a candidate task spec and return its next run times WITHOUT creating anything. ALWAYS call this before create_task and show the user the result. IMPORTANT: scheduled runs are fresh background sessions with NO memory of this conversation — put every ID, URL, and name the run needs into the intent text.',
158
+ 'Validate a candidate task spec and return its next run times WITHOUT creating anything. ALWAYS call this before create_task and show the user the result.',
164
159
  schema: previewInput,
165
160
  },
166
161
  );
@@ -190,8 +185,8 @@ function createTask(): PluginTool {
190
185
  {
191
186
  name: 'create_task',
192
187
  description:
193
- 'Schedule a previewed task. Call this ONLY after the user has seen the preview_task output and confirmed in a NEW message — never in the same turn as preview_task. TRIGGER CHOICE: "in 10 minutes", "tomorrow at 5pm" = kind "once" (compute `at` from now) — cron is ONLY for genuinely recurring schedules; `*/10 * * * *` means every-10-minutes-forever, not once-in-10-minutes. Set `approval` to "before-action" when a run would send/post/publish/create anything on the user\'s behalf: each fire then posts an approval request into their room and runs only once they approve. Runs are FRESH background sessions with no memory of this conversation — every ID/URL/name the run needs must be in the intent.',
194
- schema: createInput,
188
+ 'Schedule a previewed task, with the same fields as preview_task. Call this ONLY after the user has seen the preview_task output and confirmed in a NEW message, never in the same turn as preview_task. Set `approval` to "before-action" when a run would send, post, publish or create anything on the user\'s behalf.',
189
+ schema: modelFacingSchema(createInput, { descriptions: false }),
195
190
  },
196
191
  );
197
192
  }
@@ -311,8 +306,8 @@ function updateTask(): PluginTool {
311
306
  {
312
307
  name: 'update_task',
313
308
  description:
314
- 'Patch a task — title, schedule, approval mode, or intent (what the task does). Changing the intent changes what every future run executes, so show the user the revised intent and get their confirmation first. Changing the schedule reschedules the next run automatically.',
315
- schema: updateInput,
309
+ 'Patch a task: title, schedule, approval or intent, as in preview_task. A new intent changes what every future run executes: show it to the user and get their confirmation first. A new schedule reschedules the next run.',
310
+ schema: modelFacingSchema(updateInput, { descriptions: false }),
316
311
  },
317
312
  );
318
313
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * `set_user_preferences` — the Node runtime's tool, verbatim in schema and
3
- * description (the description is prompt material the model has been tuned
4
- * against; keep it in sync with `packages/oracle-runtime`). The handler
5
- * writes through the host's `ctx.preferences` surface for the active room.
2
+ * `set_user_preferences`: the Node runtime's tool, the same schema with
3
+ * shorter text (the model reads it on every call). The handler writes
4
+ * through the host's `ctx.preferences` surface for the active room.
6
5
  */
7
6
  import { z } from 'zod';
8
7
  import { tool } from '../../plugin-api/tool-helper';
@@ -26,67 +25,42 @@ export const setUserPreferencesSchema = z.object({
26
25
  .string()
27
26
  .max(80)
28
27
  .optional()
29
- .describe(
30
- "What the user wants you (the agent) to be called (e.g. 'Companion'). Use exactly the form the user asked for. Max 80 characters.",
31
- ),
28
+ .describe('What the user wants to call you, in exactly their form.'),
32
29
  userName: z
33
30
  .string()
34
31
  .max(80)
35
32
  .optional()
36
- .describe(
37
- "What the user wants you to call them (e.g. 'Yousef'). Use exactly the form the user asked for. Max 80 characters.",
38
- ),
33
+ .describe('What the user wants to be called, in exactly their form.'),
39
34
  language: z
40
35
  .string()
41
36
  .max(20)
42
37
  .optional()
43
38
  .describe(
44
- "Preferred reply language as a free-form code or name (e.g. 'en', 'ar', 'Arabic', 'Egyptian Arabic'). " +
45
- 'Be precise — if the user specified a dialect or variant, capture that. Max 20 characters.',
39
+ "Reply language, code or name, with any dialect (e.g. 'ar', 'Egyptian Arabic').",
46
40
  ),
47
41
  tone: z
48
42
  .string()
49
43
  .max(MAX_TONE_LENGTH)
50
44
  .optional()
51
45
  .describe(
52
- "Short tone label your future self will see at the top of every prompt (e.g. 'playful and warm', " +
53
- `'concise and dry', 'patient teacher'). Pick descriptive words, not vague ones. Max ${MAX_TONE_LENGTH} characters.`,
46
+ "Short descriptive tone label (e.g. 'playful and warm', 'concise and dry').",
54
47
  ),
55
48
  formality: z
56
49
  .enum(FORMALITY_LEVELS)
57
50
  .optional()
58
- .describe(
59
- 'How formal the replies should be. Pick the value closest to what the user actually wants, ' +
60
- 'not what the request literally said.',
61
- ),
51
+ .describe('The value closest to what the user actually wants.'),
62
52
  customInstructions: z
63
53
  .string()
64
54
  .max(2000)
65
55
  .optional()
66
56
  .describe(
67
- 'Free-form custom instructions your FUTURE SELF will read every turn. Treat this like writing ' +
68
- 'a system prompt: be specific, complete, and unambiguous. Include the WHY when it matters ' +
69
- "(e.g. 'User has dyslexia — keep sentences short and avoid long lists'). " +
70
- 'When updating, MERGE with existing instructions rather than replacing — read what is already ' +
71
- 'there (visible in your current system prompt under "User Preferences") and write the full new ' +
72
- 'instruction set, preserving anything still relevant. Max 2000 characters.',
57
+ 'Instructions you will read on every turn, like a system prompt: specific, complete, with the why when it matters (e.g. \'User has dyslexia: short sentences, no long lists\'). This field is replaced whole: write the full new set, keeping what is still relevant from "User Preferences" in your system prompt.',
73
58
  ),
74
59
  });
75
60
 
76
61
  export const SET_USER_PREFERENCES_DESCRIPTION =
77
- "Update the user's preferences. Call this whenever the user asks you to change how you behave " +
78
- "(e.g. 'call me Yousef', 'reply in Arabic', 'be more casual').\n\n" +
79
- 'IMPORTANT — you are writing instructions for your FUTURE SELF. ' +
80
- 'On every later turn this exact text will be injected back into your system prompt and you will read it cold, ' +
81
- 'with no memory of this conversation or why the user asked. So:\n' +
82
- " • Be specific and self-contained. Don't write 'be more casual' — write 'Use casual, conversational " +
83
- "tone. Avoid corporate phrasing. Use contractions. Drop filler words.'\n" +
84
- " • Capture intent, not just the literal request. If the user says 'stop being so formal', infer the " +
85
- 'positive form: what should you do instead?\n' +
86
- ' • Preserve existing instructions when adding new ones — only overwrite the fields you mean to change. ' +
87
- 'Other fields are kept as-is.\n' +
88
- " • If unsure between two interpretations, pick the more specific one — your future self can't ask back.\n\n" +
89
- "Note: changes apply from the user's NEXT message (the current turn's prompt is already built).";
62
+ "Save how the user wants you to behave (e.g. 'call me Yousef', 'reply in Arabic', 'be more casual'). Takes effect from their NEXT message. Only the fields you pass change.\n\n" +
63
+ "You are writing for your FUTURE SELF, who reads this text cold on every later turn with no memory of why. Be specific and self-contained: not 'be more casual' but 'Use a casual, conversational tone. Use contractions. Avoid corporate phrasing.' Capture the intent as what to do ('stop being so formal' becomes what to do instead). Between two readings pick the more specific one.";
90
64
 
91
65
  export interface CreateSetUserPreferencesToolOptions {
92
66
  logger?: Logger;
@@ -79,7 +79,11 @@ const manifest: PluginManifest = {
79
79
  args: { tone: 'concise' },
80
80
  },
81
81
  ],
82
+ // The Tier-1 entry stays, so the prompt says when a preference must be
83
+ // saved; only the tool waits for a load.
82
84
  visibility: 'always',
85
+ brief: 'save lasting reply preferences: name, language, tone',
86
+ onDemandTools: [SET_USER_PREFERENCES_TOOL_NAME],
83
87
  stability: 'stable',
84
88
  category: 'core',
85
89
  };
@@ -172,6 +172,11 @@ async function guard(
172
172
  try {
173
173
  return await fn();
174
174
  } catch (err) {
175
+ if (err instanceof VfsAuthError && err.kind !== 'no-delegation') {
176
+ // The user granted access but no bearer could be made (the UCAN store
177
+ // or the mint failed): the model only reads a generic sentence.
178
+ ctx.logger.warn(`[vfs] ${err.kind}: ${err.message}`);
179
+ }
175
180
  if (err instanceof VfsHttpError || err instanceof VfsAuthError) {
176
181
  return mapVfsError(err, { ...info, oracleDid: readOracleDid(ctx) });
177
182
  }
@@ -384,11 +389,8 @@ export function createVfsTools(deps: CreateVfsToolsDeps): PluginTool[] {
384
389
  {
385
390
  name: 'vfs_search',
386
391
  description:
387
- "Find files by meaning across the user's filesystem. Use for questions and paraphrases ('the doc about pricing'). Returns file paths + cited line ranges. Follow with `vfs_read` to see the actual lines before answering or editing. For writing, editing, organising, or sharing files, load the `vfs` capability first.",
392
+ "Find files by meaning across the user's filesystem. Use for questions and paraphrases ('the doc about pricing'). Returns file paths + cited line ranges. Follow with `vfs_read` to see the actual lines before answering or editing.",
388
393
  schema: searchSchema,
389
- // Recall must work with zero load steps — this tool and `vfs_read`
390
- // stay visible while the rest of the plugin is on-demand.
391
- visibility: 'always',
392
394
  },
393
395
  );
394
396
 
@@ -471,8 +473,6 @@ export function createVfsTools(deps: CreateVfsToolsDeps): PluginTool[] {
471
473
  description:
472
474
  "Read a file's contents by path. Text files come back a window of numbered lines at a time (default first 2000; page with `offset` when `hasMore`). Images and PDFs are read too — described/transcribed for you. Always read before editing or answering from a file; do not guess its contents. Reading an image costs an extra step, so read it only when you need to see it.",
473
475
  schema: readSchema,
474
- // See vfs_search — the read half of the always-available recall pair.
475
- visibility: 'always',
476
476
  },
477
477
  );
478
478
 
@@ -74,12 +74,10 @@ const manifest: PluginManifest = {
74
74
  summary:
75
75
  "The user's Virtual Filesystem (VFS) — their persistent, secure, governed filesystem: the canonical home for their real documents, notes, datasets, and artifacts, in folders, searchable and versioned linked to their domain. Reusable context that lives across sessions — not scratch files, not chat attachments, not the web. Securely stored and access-controlled (managed encryption); authorized IXO services can read content to power search, previews, and these tools, so it is NOT end-to-end encrypted — never describe it as zero-knowledge or unreadable by IXO.",
76
76
  category: 'data',
77
- // On-demand with two exceptions: `vfs_search` and `vfs_read` carry per-tool
78
- // `visibility: 'always'` so recall ("what did my notes say…") works with no
79
- // load step. The write/organise/share surface (and the sandbox bridges)
80
- // loads via `load_capability(['vfs'])` — keeping ~11 tool schemas out of
81
- // every unrelated chat turn.
77
+ // Every tool, recall included, loads via `load_capability(['vfs'])`: the
78
+ // catalogue line names the files, so a question about them costs one load.
82
79
  visibility: 'on-demand',
80
+ brief: "the user's library: save, read, search, organise files at any path",
83
81
  whenToUse: [
84
82
  "The user refers to a document, note, file, folder, or something they 'saved', 'uploaded', or 'shared with you'.",
85
83
  'You need to create, update, or organise a file for the user (draft, report, notes, export) that should persist.',
@@ -95,6 +93,7 @@ const manifest: PluginManifest = {
95
93
  "Content the user pasted directly into chat (act on that inline; don't write it to a file unless asked).",
96
94
  "Temporary/scratch or intermediate files produced during a code or compute run — those belong to the sandbox, not the user's filesystem.",
97
95
  'Another plugin owns the data (flows, skills, memory) — use that plugin.',
96
+ "The oracle's own material (its handbook, policies, procedures, reference documents): that is oracle knowledge, found with the memory search, not in the user's files.",
98
97
  ],
99
98
  examples: [
100
99
  {
@@ -661,6 +661,7 @@ const manifest: PluginManifest = {
661
661
  category: 'data',
662
662
  // Yoma's assistant sets `visibility: 'always'` through `manifestOverrides`.
663
663
  visibility: 'on-demand',
664
+ brief: 'Yoma programmes and jobs; apply, log work, check payment',
664
665
  stability: 'beta',
665
666
  };
666
667