@matatbread/matbot-core 0.4.19 → 0.4.20

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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.4.19",
7
+ "version": "0.4.20",
8
8
  "engines": {
9
9
  "node": ">=24.0.0"
10
10
  },
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@matatbread/matbot-plugin-api": "^0.4.19"
38
+ "@matatbread/matbot-plugin-api": "^0.4.20"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^6.0.3"
package/src/assemble.ts CHANGED
@@ -69,8 +69,6 @@ export interface AssembleOptions {
69
69
  providers: ProviderRegistryImpl;
70
70
  gate: HostGate;
71
71
  defaultSettings?: ReadonlyMap<string, Readonly<Record<string, unknown>>> | undefined;
72
- /** Registry values present from boot, which an unregister reverts to rather than deleting. */
73
- seed?: Partial<MatbotServices>;
74
72
  builtinTools?: Tool[];
75
73
  /** Resolve one `${NAME}` reference; the browser prompts for a missing secret. Defaults to the vault. */
76
74
  resolveSecret?(ref: string, vault: Vault): Promise<string>;
@@ -183,6 +181,9 @@ export function assembleMachine(opts: AssembleOptions): AssembledMachine {
183
181
  // registers its own (forwarding to its parent) over it — and unloading that reverts to refusing, which
184
182
  // is the honest answer for a job with no channel. Late-bound, as `services` is built below. It writes
185
183
  // through the shared runner, the one writer of the sessions in `services.sessions`.
184
+ // Not a host-supplied set: an embedder with a service of its own registers it on the returned
185
+ // `services` before `loaded()`, which is the same moment and one mechanism rather than two. A `seed`
186
+ // option existed for the node `FunctionRunner`, and went with it when that became a plugin.
186
187
  const seed: Partial<MatbotServices> = {
187
188
  MediaStore: fileStore,
188
189
  SessionAppender: createSessionAppender({
@@ -191,7 +192,6 @@ export function assembleMachine(opts: AssembleOptions): AssembledMachine {
191
192
  notifier: () => services.Notifier,
192
193
  isSubAgent: () => services.isSubAgent(),
193
194
  }),
194
- ...opts.seed,
195
195
  };
196
196
  const serviceRegistry = new Map<string, unknown>(Object.entries(seed));
197
197
  // The validator lookup is late-bound and read per call: one is registered by a plugin long after the
@@ -21,12 +21,6 @@ export interface MatbotConfig {
21
21
  * config rather than data. A stored value always wins; `delete` reverts to the default here.
22
22
  */
23
23
  defaultSettings?: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
24
- /**
25
- * Longest a `tool_function` body may run synchronously — without awaiting — before it is stopped
26
- * (`function_timeout_ms`). Absent ⇒ the host's default. `0` ⇒ no limit at all, for testing: bodies run
27
- * directly, and a loop that never awaits freezes the process.
28
- */
29
- functionTimeoutMs?: number;
30
24
  }
31
25
 
32
26
  function asString(v: YamlValue | undefined, label: string): string {
@@ -144,17 +138,6 @@ export function parseConfig(
144
138
  }
145
139
  }
146
140
 
147
- const functionTimeoutRaw = doc['function_timeout_ms'];
148
- let functionTimeoutMs: number | undefined;
149
- if (functionTimeoutRaw !== undefined && functionTimeoutRaw !== null) {
150
- functionTimeoutMs = asNumber(functionTimeoutRaw, 'function_timeout_ms');
151
- // Refused rather than clamped, like maxRounds: a negative or fractional limit is a typo, and `0` already
152
- // means something of its own.
153
- if (!Number.isInteger(functionTimeoutMs) || functionTimeoutMs < 0) {
154
- throw new Error(`Config: "function_timeout_ms" must be a non-negative integer (milliseconds; 0 removes the limit), got ${functionTimeoutMs}`);
155
- }
156
- }
157
-
158
141
  const prompt = typeof doc['prompt'] === 'string' ? doc['prompt'] : undefined;
159
142
  const ephemeral = doc['ephemeral'] === true ? true : undefined;
160
143
  const defaultProvider = typeof doc['default_provider'] === 'string' ? doc['default_provider'] : undefined;
@@ -168,7 +151,6 @@ export function parseConfig(
168
151
  ...(defaultProvider !== undefined ? { defaultProvider } : {}),
169
152
  ...(principal !== undefined ? { principal } : {}),
170
153
  ...(defaultSettings.size > 0 ? { defaultSettings } : {}),
171
- ...(functionTimeoutMs !== undefined ? { functionTimeoutMs } : {}),
172
154
  };
173
155
  }
174
156
 
package/src/index.ts CHANGED
@@ -18,12 +18,12 @@ export type {
18
18
  MissingSecretError, IncompatibleRuntimeError, NotAPluginError, PromptCancelledError, ReadOnlyError,
19
19
  MediaRejectedError, InvalidSecretNameError, MatbotErrorKind,
20
20
  } from '@matatbread/matbot-plugin-api';
21
- export { applyCreateSecret, assertStorableKey, unreferenceableKey, REFERENCEABLE_KEY_RULE } from '@matatbread/matbot-plugin-api';
21
+ export { applyResolve, applyCreateSecret, assertStorableKey, unreferenceableKey, REFERENCEABLE_KEY_RULE } from '@matatbread/matbot-plugin-api';
22
22
  // The `provider update` patch policy, alongside the vault's: written once in plugin-api so the hosts
23
23
  // that apply one cannot differ on what a patch MEANS, and re-exported here so an app needs no direct
24
24
  // plugin-api dependency to get it.
25
25
  export { applyProviderPatch, patchedFields } from '@matatbread/matbot-plugin-api';
26
- export { isTruncatedToolResult, notifyingStore, ItemChangeKind, RegistryChangeKind, SessionAppendKind, FUNCTION_TIMEOUT } from '@matatbread/matbot-plugin-api';
26
+ export { isTruncatedToolResult, notifyingStore, ItemChangeKind, RegistryChangeKind, SessionAppendKind, FUNCTION_TIMEOUT, permitted, refused, refusalMessage } from '@matatbread/matbot-plugin-api';
27
27
  // The canonical `type: 'confirm'` answers. Same reason as the patch policy above: an app implementing a
28
28
  // PromptFn (or a test standing one in) has to speak them, and must not hard-code the rendered label.
29
29
  export { CONFIRM_YES, CONFIRM_NO } from '@matatbread/matbot-plugin-api';
package/src/registry.ts CHANGED
@@ -60,7 +60,7 @@ async function resolveToolCollision(
60
60
  ): Promise<boolean> {
61
61
  const owner = existingOwner !== undefined ? `"${existingOwner}"` : 'a built-in';
62
62
  const label = `Tool \`"${toolName}"\` is already registered by **${owner}**. Overwrite it with the one from **"${incomingOwner}"**?`;
63
- const allowed = await (services.PermissionGate ?? askPermissionGate).decide({
63
+ const decision = await (services.PermissionGate ?? askPermissionGate).decide({
64
64
  gate: 'tools.overwrite',
65
65
  subject: toolName,
66
66
  // The incoming OWNER rides in the label rather than in a field of its own: a single `subject`
@@ -73,9 +73,10 @@ async function resolveToolCollision(
73
73
  // changed hands" is exactly the report this line answers. A policy that overwrites after ASKING
74
74
  // needs no warning — the user just saw the question.
75
75
  if (prompt === undefined) {
76
- console.warn(`[matbot] ${label} — non-interactive, ${allowed ? 'overwriting' : 'keeping the existing tool'}.`);
76
+ console.warn(`[matbot] ${label} — non-interactive (${decision.source}), `
77
+ + `${decision.allowed ? 'overwriting' : 'keeping the existing tool'}.`);
77
78
  }
78
- return allowed;
79
+ return decision.allowed;
79
80
  }
80
81
 
81
82
  // ── Version check ─────────────────────────────────────────────────────────────
package/src/runner.ts CHANGED
@@ -192,6 +192,7 @@ export async function* runSession(opts: RunSessionOpts): AsyncIterable<TurnEvent
192
192
  async createSecret() { throw new Error('No vault configured'); },
193
193
  async writeSecret() { throw new Error('No vault configured'); },
194
194
  hasKey() { return false; },
195
+ async readSecret() { return undefined; },
195
196
  async resolve(ref: string) { return ref; },
196
197
  scrub(text: string) { return text; },
197
198
  };
@@ -449,6 +450,9 @@ export async function* runSession(opts: RunSessionOpts): AsyncIterable<TurnEvent
449
450
  case 'thinking':
450
451
  yield { type: 'thinking', delta: ev.delta, traceId };
451
452
  break;
453
+ case 'progress':
454
+ yield { type: 'progress', pct: ev.pct, ...(ev.message !== undefined ? { message: ev.message } : {}), traceId };
455
+ break;
452
456
  case 'thinking-block':
453
457
  assistantParts.push({ type: 'thinking', thinking: ev.thinking, signature: ev.signature });
454
458
  break;
@@ -668,7 +672,7 @@ export async function* runSession(opts: RunSessionOpts): AsyncIterable<TurnEvent
668
672
  // default, so a gate handed it could never tell "no human is reachable" from "a human answered
669
673
  // with the default" — and would get the fallback back one layer too late to reason about.
670
674
  // `ToolContext.prompt` keeps the stand-in exactly as before; only the gate sees the truth.
671
- ...bindGate(opts.permissionGate, tc.name, opts.prompt),
675
+ ...bindGate(opts.permissionGate, tool, opts.prompt, opts.toolRegistry ?? { resolve: n => tools.get(n) ?? null }),
672
676
  ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),
673
677
  ...(opts.configPath !== undefined ? { configPath: opts.configPath } : {}),
674
678
  ...(opts.files !== undefined ? { files: opts.files } : {}),
@@ -1,7 +1,6 @@
1
1
  import type { Vault } from '@matatbread/matbot-plugin-api';
2
- import { missingSecretError, applyCreateSecret, assertStorableKey, unreferenceableKey } from '@matatbread/matbot-plugin-api';
2
+ import { applyResolve, applyCreateSecret, assertStorableKey, unreferenceableKey } from '@matatbread/matbot-plugin-api';
3
3
 
4
- const REF_RE = /\$\{([^}]+)\}/g;
5
4
 
6
5
  /**
7
6
  * Default vault implementation: a single flat namespace of secrets keyed by name.
@@ -58,19 +57,12 @@ export class VaultImpl implements Vault {
58
57
  return undefined;
59
58
  }
60
59
 
61
- async resolve(ref: string): Promise<string> {
62
- const errors: string[] = [];
63
-
64
- const result = ref.replace(REF_RE, (_, name: string) => {
65
- const value = this.store.get(name);
66
- if (value === undefined) { errors.push(name); return ''; }
67
- return value;
68
- });
60
+ async readSecret(name: string): Promise<string | undefined> {
61
+ return this.store.get(name);
62
+ }
69
63
 
70
- if (errors.length > 0) {
71
- throw missingSecretError(errors);
72
- }
73
- return result;
64
+ resolve(ref: string): Promise<string> {
65
+ return applyResolve(this, ref);
74
66
  }
75
67
 
76
68
  scrub(text: string): string {