@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 +2 -2
- package/src/assemble.ts +3 -3
- package/src/config/loader.ts +0 -18
- package/src/index.ts +2 -2
- package/src/registry.ts +4 -3
- package/src/runner.ts +5 -1
- package/src/security/vault.ts +6 -14
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.4.
|
|
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.
|
|
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
|
package/src/config/loader.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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 } : {}),
|
package/src/security/vault.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Vault } from '@matatbread/matbot-plugin-api';
|
|
2
|
-
import {
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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 {
|