@ixo/oracle-runtime-workers 0.13.0 → 0.16.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 (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +13 -8
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +310 -0
  18. package/src/channels/contract.ts +246 -0
  19. package/src/channels/turns.ts +280 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +170 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +2003 -274
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +504 -108
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +562 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. package/src/tasks/topic-deliverables.ts +70 -0
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Plugin authoring surface. A plugin written against `@ixo/oracle-runtime`
3
3
  * that uses tools / sub-agents / middlewares / manifest / configSchema /
4
- * autoDetect / getSharedState compiles unchanged against this module; the
5
- * only Node-specific hook (`getNestModules`) is replaced by `getRoutes`.
4
+ * autoDetect / getSharedState / getDecisions compiles unchanged against this
5
+ * module; the only Node-specific hook (`getNestModules`) is replaced by
6
+ * `getRoutes`.
6
7
  */
7
8
  export { OraclePlugin } from './oracle-plugin';
9
+ export type {
10
+ RequestAdmissionContext,
11
+ RequestAdmissionResult,
12
+ RequestDisposition,
13
+ } from './request-admission';
8
14
  export type {
9
15
  PluginEnv,
10
16
  PluginRoute,
@@ -13,8 +19,26 @@ export type {
13
19
  export { defineOraclePlugin } from './define-plugin';
14
20
  export type { DefineOraclePluginInput } from './define-plugin';
15
21
  export { tool } from './tool-helper';
22
+ export {
23
+ ADMIN_TOOL_ACTION,
24
+ ADMIN_TOOL_RESOURCE,
25
+ adminToolCapability,
26
+ adminToolResource,
27
+ canAccessToolPlane,
28
+ requireToolPlane,
29
+ toolPlaneOf,
30
+ toolPlaneRequirement,
31
+ } from './tool-plane';
16
32
  export type { ToolHelperOptions } from './tool-helper';
17
33
  export { UcanMintUnavailableError } from './ucan-errors';
34
+ export {
35
+ USER_KV_MAX_ENTRIES_PER_NAMESPACE,
36
+ USER_KV_MAX_TOTAL_BYTES,
37
+ USER_KV_MAX_TOTAL_ENTRIES,
38
+ USER_KV_MAX_VALUE_BYTES,
39
+ UserKvLimitError,
40
+ } from '../core/user-kv';
41
+ export type { UserKvLimitKind } from '../core/user-kv';
18
42
  export type {
19
43
  ActionCallEventPayload,
20
44
  AgentMiddleware,
@@ -49,8 +73,25 @@ export type {
49
73
  RouterEventPayload,
50
74
  RuntimeContext,
51
75
  SecretIndex,
76
+ SessionSurface,
52
77
  SharedAccessors,
53
78
  ToolCallEventPayload,
79
+ ToolPlane,
54
80
  UcanDelegation,
55
81
  UserContextData,
82
+ UserKvSurface,
83
+ UserKvWriteOptions,
56
84
  } from './types';
85
+ export { defineDecision } from '@ixo/common/ai/decisions';
86
+ export type {
87
+ DecisionAdapter,
88
+ DecisionAnswer,
89
+ DecisionDefinition,
90
+ DecisionEvaluateOptions,
91
+ DecisionEvaluation,
92
+ DecisionProviderResult,
93
+ DecisionQuestion,
94
+ DecisionRegistration,
95
+ DecisionRequest,
96
+ DefineDecisionOptions,
97
+ } from '@ixo/common/ai/decisions';
@@ -1,3 +1,8 @@
1
+ import type { DecisionRegistration } from '@ixo/common/ai/decisions';
2
+ import type {
3
+ RequestAdmissionContext,
4
+ RequestAdmissionResult,
5
+ } from './request-admission';
1
6
  import type { z } from 'zod';
2
7
  import type { PluginRoute } from '../shell/app';
3
8
  import type {
@@ -53,6 +58,19 @@ export abstract class OraclePlugin {
53
58
  /** The agent's structured interface to this plugin. */
54
59
  abstract readonly manifest: PluginManifest;
55
60
 
61
+ /**
62
+ * How the agent should work with this plugin once it is in use: the
63
+ * plugin's working rules, as plain text (markdown, starting at a `###`
64
+ * heading). Never part of the manifest, the Tier-1 capability block or
65
+ * `list_capabilities`, so it costs nothing on turns that do not use the
66
+ * plugin. `load_capability` returns it (once per turn) when it loads the
67
+ * plugin or the capability router preloaded it for the turn, and the
68
+ * system prompt carries it on every turn that starts with the plugin
69
+ * loaded on the thread (or `always` visible) and usable by the user's
70
+ * delegation.
71
+ */
72
+ readonly operatingGuide?: string;
73
+
56
74
  /** Hard dependencies — boot fails if any is missing. */
57
75
  readonly dependsOn?: string[];
58
76
 
@@ -83,6 +101,13 @@ export abstract class OraclePlugin {
83
101
  /** Sub-agents the runtime auto-wraps as tools. */
84
102
  getSubAgents?(ctx: PluginContext): PluginSubAgent[];
85
103
 
104
+ /**
105
+ * Bounded semantic decisions contributed by this plugin. Decisions are
106
+ * side-effect-free and are never exposed to the generative agent unless a
107
+ * plugin explicitly wraps one in a tool.
108
+ */
109
+ getDecisions?(ctx: PluginContext): DecisionRegistration[];
110
+
86
111
  /**
87
112
  * Request-time tool contributions. Called once per `createMainAgent`
88
113
  * invocation with the full per-request `RuntimeContext`, so the plugin
@@ -110,6 +135,16 @@ export abstract class OraclePlugin {
110
135
  */
111
136
  getMiddlewares?(ctx: PluginContext): AgentMiddleware[];
112
137
 
138
+ /** Per-turn extensions for the ordinary agent; never cached across users. */
139
+ getRequestMiddlewares?(
140
+ ctx: RuntimeContext,
141
+ ): AgentMiddleware[] | Promise<AgentMiddleware[]>;
142
+
143
+ /** Runs before agent preparation. Only authorized deterministic reads may be handled. */
144
+ getRequestAdmission?(
145
+ ctx: RequestAdmissionContext,
146
+ ): RequestAdmissionResult | Promise<RequestAdmissionResult>;
147
+
113
148
  /**
114
149
  * Read-only accessors this plugin exposes to other plugins via `ctx.shared`.
115
150
  * Pattern: this plugin computes/owns some derived value from state; others
@@ -0,0 +1,41 @@
1
+ import type { MergedConfig, RuntimeContext } from './types';
2
+
3
+ /**
4
+ * Authenticated, request-local input. Deliberately exposes no model or tools.
5
+ *
6
+ * `config` is not the merged env: it carries the plugin's own `configSchema`
7
+ * keys and the core settings without credentials (no LLM provider key, no
8
+ * Matrix password or recovery phrase, no Cloudflare API token).
9
+ *
10
+ * The host only asks for admission on direct-room and Portal turns, never on
11
+ * a group-room turn (a direct read is posted into the room, where every
12
+ * member would see one user's authorized data) or a scheduled task run.
13
+ * `roomKind` is still passed so a handler can refuse anything it would not
14
+ * show to the whole room.
15
+ */
16
+ export interface RequestAdmissionContext {
17
+ readonly config: MergedConfig;
18
+ readonly user: RuntimeContext['user'];
19
+ readonly session: RuntimeContext['session'] & {
20
+ /** Matrix turns: the room the turn arrived in is a direct or a group room. */
21
+ readonly roomKind?: 'direct' | 'group';
22
+ /** Matrix turns: the user's message event this turn answers. */
23
+ readonly eventId?: string;
24
+ /** Matrix turns: the thread root when the user replied inside a thread. */
25
+ readonly threadId?: string;
26
+ };
27
+ readonly message: string;
28
+ readonly metadata?: Record<string, unknown>;
29
+ /** Aborted when the turn is cancelled or the handler's time limit passes. */
30
+ readonly signal: AbortSignal;
31
+ }
32
+
33
+ export type RequestAdmissionResult =
34
+ | { kind: 'pass' }
35
+ | { kind: 'handled'; text: string; title: string };
36
+
37
+ /** Persisted by the host, never accepted from a client. */
38
+ export type RequestDisposition =
39
+ | { kind: 'admitting' }
40
+ | { kind: 'agent' }
41
+ | { kind: 'direct-read'; text: string; title: string; messageId: string };
@@ -17,11 +17,20 @@ export interface ToolHelperOptions {
17
17
  * `manifest.visibility` setting.
18
18
  */
19
19
  visibility?: PluginTool['visibility'];
20
+ /**
21
+ * Privilege plane. Defaults to `orchestration`; `admin` marks a tool the
22
+ * user must explicitly delegate (`adminToolCapability`) before the model
23
+ * may see or run it — authority, credential or similar lifecycle
24
+ * operations. See `PluginTool.plane`.
25
+ */
26
+ plane?: PluginTool['plane'];
20
27
  /**
21
28
  * `'read'` for a tool with no external effect (safe to run again when a
22
29
  * turn resumes after a reset); omitted or `'write'` otherwise.
23
30
  */
24
31
  effect?: PluginTool['effect'];
32
+ /** Same arguments again is a new action (a UI step); see `PluginTool.repeatable`. */
33
+ repeatable?: PluginTool['repeatable'];
25
34
  /**
26
35
  * Billing gate marker, kept for source compatibility with the Node
27
36
  * runtime's plugin contract. The Workers runtime has no commerce lane, so
@@ -54,7 +63,16 @@ export function tool(
54
63
  if (!options || typeof options !== 'object') {
55
64
  throw new TypeError('tool(handler, options): `options` is required.');
56
65
  }
57
- const { name, description, schema, visibility, billing, effect } = options;
66
+ const {
67
+ name,
68
+ description,
69
+ schema,
70
+ visibility,
71
+ plane,
72
+ billing,
73
+ effect,
74
+ repeatable,
75
+ } = options;
58
76
  if (!name || typeof name !== 'string') {
59
77
  throw new TypeError(
60
78
  'tool(handler, options): `options.name` must be a non-empty string.',
@@ -80,11 +98,17 @@ export function tool(
80
98
  if (visibility !== undefined) {
81
99
  pluginTool.visibility = visibility;
82
100
  }
101
+ if (plane !== undefined) {
102
+ pluginTool.plane = plane;
103
+ }
83
104
  if (billing !== undefined) {
84
105
  pluginTool.billing = billing;
85
106
  }
86
107
  if (effect !== undefined) {
87
108
  pluginTool.effect = effect;
88
109
  }
110
+ if (repeatable !== undefined) {
111
+ pluginTool.repeatable = repeatable;
112
+ }
89
113
  return pluginTool;
90
114
  }
@@ -0,0 +1,92 @@
1
+ import type { PluginTool, RuntimeContext, ToolPlane } from './types';
2
+
3
+ /**
4
+ * The ability every admin-tool capability names. Namespaced because a UCAN
5
+ * ability must be `*` or `<ns>/<name>` (`@ixo/ucan` refuses to issue a bare
6
+ * `invoke`); a grant of `admin-tool/*` or `*` covers it too.
7
+ */
8
+ export const ADMIN_TOOL_ACTION = 'admin-tool/invoke';
9
+
10
+ /**
11
+ * Root of the admin-tool resources. One admin tool is
12
+ * `ixo:qiforge:admin-tool/<pluginName>/<toolName>`; under the runtime's
13
+ * resource rule (a grant covers itself and every `/` child, `*` covers
14
+ * everything) a grant on `ixo:qiforge:admin-tool/<pluginName>` covers all of
15
+ * that plugin's admin tools and one on this root covers every admin tool.
16
+ */
17
+ export const ADMIN_TOOL_RESOURCE = 'ixo:qiforge:admin-tool';
18
+
19
+ type ToolPlaneDescriptor = Pick<PluginTool, 'name' | 'plane'>;
20
+
21
+ /** A tool's plane; undeclared is `orchestration`. */
22
+ export function toolPlaneOf(tool: Pick<PluginTool, 'plane'>): ToolPlane {
23
+ return tool.plane ?? 'orchestration';
24
+ }
25
+
26
+ function segment(kind: string, value: string): string {
27
+ if (value.length === 0 || value.includes('/')) {
28
+ // A `/` would make one tool's resource the parent of another's (plugin
29
+ // `a` tool `b/c` vs plugin `a/b` tool `c`), so the grant rule could no
30
+ // longer tell them apart.
31
+ throw new TypeError(
32
+ `admin tool ${kind} must be a non-empty name without "/" (got "${value}")`,
33
+ );
34
+ }
35
+ return value;
36
+ }
37
+
38
+ /** `ixo:qiforge:admin-tool/<pluginName>/<toolName>`. */
39
+ export function adminToolResource(
40
+ pluginName: string,
41
+ toolName: string,
42
+ ): string {
43
+ return `${ADMIN_TOOL_RESOURCE}/${segment('plugin name', pluginName)}/${segment('tool name', toolName)}`;
44
+ }
45
+
46
+ /** The capability the user's delegation must grant for this admin tool. */
47
+ export function adminToolCapability(
48
+ pluginName: string,
49
+ toolName: string,
50
+ ): { resource: string; action: string } {
51
+ return {
52
+ resource: adminToolResource(pluginName, toolName),
53
+ action: ADMIN_TOOL_ACTION,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * The capability `tool` requires of the turn's delegation, or `null` for an
59
+ * orchestration tool. This is the tool-level counterpart of a manifest's
60
+ * `requires`; every check (binding, discovery, the call itself) reads it.
61
+ */
62
+ export function toolPlaneRequirement(
63
+ pluginName: string,
64
+ tool: ToolPlaneDescriptor,
65
+ ): { resource: string; action: string } | null {
66
+ return toolPlaneOf(tool) === 'admin'
67
+ ? adminToolCapability(pluginName, tool.name)
68
+ : null;
69
+ }
70
+
71
+ /** Whether the delegation behind `ctx` reaches `tool`. */
72
+ export function canAccessToolPlane(
73
+ ctx: Pick<RuntimeContext, 'ucan'>,
74
+ pluginName: string,
75
+ tool: ToolPlaneDescriptor,
76
+ ): boolean {
77
+ const required = toolPlaneRequirement(pluginName, tool);
78
+ return (
79
+ required === null ||
80
+ ctx.ucan.hasCapability(required.resource, required.action)
81
+ );
82
+ }
83
+
84
+ /** Throws (`ctx.ucan.requireCapability`) unless the delegation behind `ctx` reaches `tool`. */
85
+ export function requireToolPlane(
86
+ ctx: Pick<RuntimeContext, 'ucan'>,
87
+ pluginName: string,
88
+ tool: ToolPlaneDescriptor,
89
+ ): void {
90
+ const required = toolPlaneRequirement(pluginName, tool);
91
+ if (required) ctx.ucan.requireCapability(required.resource, required.action);
92
+ }
@@ -1,8 +1,15 @@
1
+ import type { TaskExecutionProfile } from '../core/execution-profile';
2
+ import type {
3
+ DecisionDefinition,
4
+ DecisionEvaluateOptions,
5
+ DecisionEvaluation,
6
+ } from '@ixo/common/ai/decisions';
1
7
  import type { z } from 'zod';
2
8
  import type { BaseMessage } from '@langchain/core/messages';
3
9
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
4
10
  import type { UserPreferences } from '../core/state';
5
11
  import type { AttachmentViewSurface } from '../attachments/view';
12
+ import type { DeliveryConfig } from '../delivery/profile';
6
13
 
7
14
  /**
8
15
  * Route shape returned by `OraclePlugin.getAuthExcludedRoutes()`. Paths are
@@ -55,6 +62,13 @@ export interface UcanDelegation {
55
62
  readonly issuer?: string;
56
63
  readonly audience?: string;
57
64
  readonly capabilities?: ReadonlyArray<{ resource: string; action: string }>;
65
+ /**
66
+ * When the delegation stops granting anything (Unix seconds; the earliest
67
+ * expiry across its proof chain). Capability checks compare it with the
68
+ * clock at every check, so a delegation that lapses mid-turn grants
69
+ * nothing from then on.
70
+ */
71
+ readonly expiration?: number;
58
72
  }
59
73
 
60
74
  /** Per-room secret index. */
@@ -360,6 +374,16 @@ export interface PluginManifest {
360
374
 
361
375
  /** Stability hint surfaced to the agent (`experimental` → warning footnote). */
362
376
  stability?: 'stable' | 'beta' | 'experimental';
377
+
378
+ /**
379
+ * UCAN capabilities the user's delegation to this oracle must grant before
380
+ * the plugin may be used by that user (`ctx.ucan.hasCapability` semantics:
381
+ * the action may be `*`). While one is missing, `load_capability` refuses
382
+ * the plugin and names what is missing, `list_capabilities` marks it
383
+ * unavailable, and its tools are neither shown to nor run by the model —
384
+ * whatever its visibility. Omitted: no requirement.
385
+ */
386
+ requires?: ReadonlyArray<{ resource: string; action: string }>;
363
387
  }
364
388
 
365
389
  export interface ManifestExample {
@@ -424,6 +448,13 @@ export interface OracleConfig {
424
448
  description?: string;
425
449
  /** Optional prompt customization (opening, communicationStyle, capabilities). */
426
450
  prompt?: OraclePromptConfig;
451
+ /**
452
+ * Chat delivery (WhatsApp through IXO Channels, Matrix rooms): replies
453
+ * arrive as short messages with long content moved to artefacts. Matrix
454
+ * rooms use it only with `matrixChat: true` (or `MATRIX_CHAT_DELIVERY=true`,
455
+ * which wins when set); otherwise a room reply stays one message.
456
+ */
457
+ delivery?: DeliveryConfig;
427
458
  }
428
459
 
429
460
  /**
@@ -476,6 +507,11 @@ export interface MatrixPostOpts {
476
507
  txnId?: string;
477
508
  }
478
509
 
510
+ /** `ctx.session.surface`: the delivery surface of the turn. */
511
+ export type SessionSurface =
512
+ | { kind: 'stream' }
513
+ | { kind: 'chat'; surface: string; label: string };
514
+
479
515
  export interface RuntimeContext<TConfig = MergedConfig> {
480
516
  /** Authenticated user identity (validated by core auth middleware). */
481
517
  user: {
@@ -491,10 +527,16 @@ export interface RuntimeContext<TConfig = MergedConfig> {
491
527
  session: {
492
528
  /** = `thread_id`; thread root `eventId`. */
493
529
  id: string;
494
- client: 'portal' | 'matrix' | 'slack';
530
+ client: 'portal' | 'matrix' | 'slack' | 'channel';
495
531
  wsId?: string;
496
532
  requestId: string;
497
533
  roomId?: string;
534
+ /**
535
+ * Where the reply lands. `stream` is the Portal; `chat` is a chat app
536
+ * (WhatsApp through IXO Channels, a Matrix room) where the reply arrives
537
+ * as short messages. Tools can keep their output compact on `chat`.
538
+ */
539
+ surface?: SessionSurface;
498
540
  };
499
541
 
500
542
  /** Read-only view over the graph state's history. */
@@ -520,6 +562,12 @@ export interface RuntimeContext<TConfig = MergedConfig> {
520
562
  secrets: {
521
563
  getIndex: () => Promise<SecretIndex>;
522
564
  getValues: (keys: string[]) => Promise<Record<string, string>>;
565
+ /**
566
+ * Values of every secret `getIndex` lists, read with one lookup of the
567
+ * room's secret index — use it instead of `getIndex` followed by
568
+ * `getValues` of every key. `{}` without a room.
569
+ */
570
+ getAll: () => Promise<Record<string, string>>;
523
571
  };
524
572
 
525
573
  /**
@@ -671,6 +719,23 @@ export interface RuntimeContext<TConfig = MergedConfig> {
671
719
  >;
672
720
  };
673
721
 
722
+ /**
723
+ * Bounded semantic decisions. A decision only returns structured judgment;
724
+ * policy, authority checks, and side effects remain outside this primitive.
725
+ */
726
+ decisions: {
727
+ evaluate<TSchema extends z.ZodType>(
728
+ definition: DecisionDefinition<TSchema>,
729
+ input: z.input<TSchema>,
730
+ options?: DecisionEvaluateOptions,
731
+ ): Promise<DecisionEvaluation>;
732
+ evaluateByName(
733
+ name: string,
734
+ input: unknown,
735
+ options?: DecisionEvaluateOptions,
736
+ ): Promise<DecisionEvaluation>;
737
+ };
738
+
674
739
  /** LLM provider. */
675
740
  llm: {
676
741
  get: (role: ModelRole, params?: ChatOpenAIFields) => BaseChatModel;
@@ -706,6 +771,15 @@ export interface RuntimeContext<TConfig = MergedConfig> {
706
771
  */
707
772
  preferences?: UserPreferencesSurface;
708
773
 
774
+ /**
775
+ * Host key-value rows in the user's OWN database, for plugin state that
776
+ * must outlive the in-memory plugin instance (on Workers the user's object
777
+ * is evicted when idle and restored from the owner copy; rows written here
778
+ * travel with that copy). Present when the host persists per-user state
779
+ * (the Workers runtime does); absent elsewhere.
780
+ */
781
+ kv?: UserKvSurface;
782
+
709
783
  /**
710
784
  * Host bridge to the user's browser over the realtime (socket.io) channel:
711
785
  * browser tools declared by the client (`state.browserTools`) and AG-UI
@@ -777,6 +851,7 @@ export type OracleTaskSchedule =
777
851
 
778
852
  /** Stored task record (persisted in the user's own database → owner file). */
779
853
  export interface OracleTaskRecord {
854
+ executionProfile?: TaskExecutionProfile;
780
855
  id: string;
781
856
  title: string;
782
857
  /** Markdown intent body — what the agent should do each run. */
@@ -799,6 +874,8 @@ export interface OracleTaskRecord {
799
874
  }
800
875
 
801
876
  export interface OracleTaskInput {
877
+ /** Host-only closed execution profile; never exposed by model task tools. */
878
+ executionProfile?: TaskExecutionProfile;
802
879
  title: string;
803
880
  intent: string;
804
881
  schedule: OracleTaskSchedule;
@@ -812,11 +889,14 @@ export interface OracleTaskInput {
812
889
  }
813
890
 
814
891
  /**
815
- * Bridge to the user's browser over the realtime channel. Both calls emit the
816
- * corresponding event (`browser_tool_call` / `action_call`) to the sockets
817
- * subscribed to `sessionId` and resolve with the browser's answer, rejecting
818
- * on the browser's `error`, an AG-UI `success: false`, or the timeout —
819
- * exactly the Node runtime's `callBrowserTool` / `callAgAction` contract.
892
+ * Bridge to the user's browser over the realtime channel. Each call is one
893
+ * invocation with its own id, sent (`browser_tool_call` / `action_call`) to
894
+ * ONE authenticated socket of `sessionId`; only that socket's answer settles
895
+ * it. It resolves with the browser's answer and rejects on the browser's
896
+ * `error`, an AG-UI `success: false`, or when no socket could take it. When
897
+ * the answer cannot arrive — the deadline passed or the socket went — it
898
+ * resolves with `FRONTEND_OUTCOME_UNKNOWN` (`@ixo/common/ai/frontend-bridge`):
899
+ * the browser may have performed the action, and it is never sent again.
820
900
  */
821
901
  export interface FrontendCallSurface {
822
902
  callBrowserTool(params: FrontendCallParams): Promise<unknown>;
@@ -827,11 +907,20 @@ export interface FrontendCallSurface {
827
907
 
828
908
  export interface FrontendCallParams {
829
909
  sessionId: string;
910
+ /** The caller's id; the invocation id the browser sees is `<toolCallId>:<uuid>`. */
830
911
  toolCallId: string;
831
912
  toolName: string;
832
913
  args: Record<string, unknown>;
833
914
  /** Default 15 s (browser tools) / 10 s (AG-UI actions), as on Node. */
834
915
  timeoutMs?: number;
916
+ /** Receives the invocation id once the call is registered (for diagnostics). */
917
+ onInvocation?: (invocationId: string) => void;
918
+ /**
919
+ * The turn's abort signal (`ctx.abortSignal`). Aborted before the call is
920
+ * sent, it rejects as not sent; after, it resolves with
921
+ * `FRONTEND_OUTCOME_UNKNOWN` at once — the browser may already have run it.
922
+ */
923
+ signal?: AbortSignal;
835
924
  }
836
925
 
837
926
  /**
@@ -861,6 +950,8 @@ export interface OracleTasksSurface {
861
950
  ): Promise<{ resolved: boolean }>;
862
951
  }
863
952
 
953
+ export type ToolPlane = 'orchestration' | 'admin';
954
+
864
955
  export interface PluginTool {
865
956
  name: string;
866
957
  description: string;
@@ -868,6 +959,15 @@ export interface PluginTool {
868
959
  handler: (args: unknown, ctx: RuntimeContext) => Promise<unknown>;
869
960
  /** Override visibility — by default inherits from the plugin's `manifest.visibility`. */
870
961
  visibility?: 'always' | 'on-demand' | 'silent';
962
+ /**
963
+ * Privilege plane. `orchestration` is the default. An `admin` tool exists
964
+ * for a turn only when the user's delegation to the oracle grants
965
+ * `adminToolCapability(pluginName, name)`: otherwise it is never bound or
966
+ * listed, and the capability is checked again right before the handler.
967
+ * That grant is the user's consent, not operator authority — a user can
968
+ * issue it for themselves.
969
+ */
970
+ plane?: ToolPlane;
871
971
  /**
872
972
  * Whether calling the tool changes anything outside the conversation.
873
973
  * `'read'` tools may be executed again when a turn is resumed after a
@@ -876,6 +976,14 @@ export interface PluginTool {
876
976
  * outcome is unknown. See `core/middlewares/tool-marks.ts`.
877
977
  */
878
978
  effect?: 'read' | 'write';
979
+ /**
980
+ * `true` when calling the tool again with the same arguments is a new,
981
+ * intended action rather than a repeat — a step in the user's browser
982
+ * such as scrolling or paging. Within one turn the repetition guard lets
983
+ * such a tool run up to its read limit (5 identical calls) instead of once;
984
+ * `effect` still governs retries, write claims and resume.
985
+ */
986
+ repeatable?: boolean;
879
987
  /**
880
988
  * Billing gate. `'contracted'` marks a tool that performs paid contracted
881
989
  * work: the runtime binds it only while the turn runs inside an active work
@@ -924,3 +1032,83 @@ export interface UserPreferencesSurface {
924
1032
  partial: Partial<UserPreferences>,
925
1033
  ): Promise<UserPreferences>;
926
1034
  }
1035
+
1036
+ /**
1037
+ * Bounds applied by a write to {@link UserKvSurface}. Both are optional; an
1038
+ * entry written without `idleTtlMs` never expires, and a write without
1039
+ * `maxEntries` evicts nothing. They come on top of the store's hard caps
1040
+ * (see {@link UserKvSurface}), which no option lifts.
1041
+ */
1042
+ export interface UserKvWriteOptions {
1043
+ /**
1044
+ * Idle lifetime: the entry expires once this long passes without a read or
1045
+ * a write of it. Every hit slides the deadline.
1046
+ */
1047
+ idleTtlMs?: number;
1048
+ /**
1049
+ * After the write, drop the namespace's expired entries, then the least
1050
+ * recently used ones until at most this many remain. At most
1051
+ * `USER_KV_MAX_ENTRIES_PER_NAMESPACE` (10,000); a larger value is rejected
1052
+ * with a `RangeError`, not clamped.
1053
+ */
1054
+ maxEntries?: number;
1055
+ }
1056
+
1057
+ /**
1058
+ * Key-value rows in the user's own database, grouped by namespace (a plugin
1059
+ * uses its own name as the prefix, e.g. `pod-creator/blueprints`). Namespaces
1060
+ * are a naming convention, not isolation: every plugin of the oracle sees the
1061
+ * same surface and can read or write any namespace. Plugins are trusted code
1062
+ * the operator chose to load; do not rely on namespaces to keep a plugin out
1063
+ * of another's rows. Values are
1064
+ * JSON: what goes in is serialised, what comes out is the parsed copy, so a
1065
+ * caller never shares an object with the store. Reads and writes mark an
1066
+ * entry most recently used and slide its idle deadline (LRU + idle TTL, the
1067
+ * semantics of an in-memory bounded map, but durable).
1068
+ *
1069
+ * The rows share the user's file with their checkpoints, sessions and
1070
+ * transcript, so the store enforces hard caps no plugin can opt out of:
1071
+ *
1072
+ * - one value: `USER_KV_MAX_VALUE_BYTES` (256 KiB) of UTF-8 JSON;
1073
+ * - one namespace: `USER_KV_MAX_ENTRIES_PER_NAMESPACE` (10,000) entries;
1074
+ * - all namespaces together: `USER_KV_MAX_TOTAL_ENTRIES` (50,000) entries
1075
+ * and `USER_KV_MAX_TOTAL_BYTES` (32 MiB), counting the UTF-8 bytes of
1076
+ * namespace, key and value JSON of every row.
1077
+ *
1078
+ * A `set` / `update` that would break one rejects with `UserKvLimitError`
1079
+ * (`limit`, `max`, `actual`, `namespace`): nothing is written and nothing
1080
+ * is evicted to make room — the previous value, if any, stays. Expired
1081
+ * entries of every namespace are swept before a write is refused, and
1082
+ * replacing an entry counts only the difference in size. Keep plugin state
1083
+ * bounded with `idleTtlMs` / `maxEntries` rather than relying on the caps.
1084
+ */
1085
+ export interface UserKvSurface {
1086
+ /** The stored value, or `undefined` when absent or expired. */
1087
+ get(namespace: string, key: string): Promise<unknown>;
1088
+ /**
1089
+ * Store a JSON-serialisable value. Rejects with `UserKvLimitError` when
1090
+ * the write would break a hard cap.
1091
+ */
1092
+ set(
1093
+ namespace: string,
1094
+ key: string,
1095
+ value: unknown,
1096
+ options?: UserKvWriteOptions,
1097
+ ): Promise<void>;
1098
+ /**
1099
+ * Atomic read-modify-write: `fn` receives the current value (`undefined`
1100
+ * when absent or expired) and returns the new one; returning `undefined`
1101
+ * deletes the entry. No other write to the store interleaves. `fn` must be
1102
+ * synchronous and free of side effects — the host may run it again when a
1103
+ * storage read has to be retried. Resolves with the value written; rejects
1104
+ * with `UserKvLimitError` (the entry left as it was) when the new value
1105
+ * would break a hard cap. Deleting is never refused.
1106
+ */
1107
+ update(
1108
+ namespace: string,
1109
+ key: string,
1110
+ fn: (current: unknown) => unknown,
1111
+ options?: UserKvWriteOptions,
1112
+ ): Promise<unknown>;
1113
+ delete(namespace: string, key: string): Promise<void>;
1114
+ }