@robota-sdk/agent-core 3.0.0-beta.79 → 3.0.0-beta.81
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/CHANGELOG.md +667 -0
- package/README.md +24 -4
- package/dist/browser/bounded-output-DpjpS98g.js +2 -0
- package/dist/browser/bounded-output-DpjpS98g.js.map +1 -0
- package/dist/browser/command-executor-cI7XJ374.js +2 -0
- package/dist/browser/command-executor-cI7XJ374.js.map +1 -0
- package/dist/browser/http-executor-CHy6UeBU.js +2 -0
- package/dist/browser/http-executor-CHy6UeBU.js.map +1 -0
- package/dist/browser/index.d.ts +3337 -1499
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +11 -11
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/verdict-decoder-Dk_w2NG9.js +2 -0
- package/dist/browser/verdict-decoder-Dk_w2NG9.js.map +1 -0
- package/dist/node/bounded-output-DpjpS98g.js +2 -0
- package/dist/node/bounded-output-DpjpS98g.js.map +1 -0
- package/dist/node/command-executor-Aat_q7P2.cjs +1 -0
- package/dist/node/command-executor-DV-H2oD6.js +2 -0
- package/dist/node/command-executor-DV-H2oD6.js.map +1 -0
- package/dist/node/http-executor-71JiIdFP.cjs +1 -0
- package/dist/node/http-executor-cWzI2svn.js +2 -0
- package/dist/node/http-executor-cWzI2svn.js.map +1 -0
- package/dist/node/{index-BKIUt9pk.d.ts → index-CiFlpr7-.d.cts} +3289 -1783
- package/dist/node/index-CiFlpr7-.d.cts.map +1 -0
- package/dist/node/index-DFlMw376.d.ts +6893 -0
- package/dist/node/index-DFlMw376.d.ts.map +1 -0
- package/dist/node/index.cjs +11 -11
- package/dist/node/index.d.cts +3 -0
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.js +11 -11
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.cjs +1 -0
- package/dist/node/node.d.cts +236 -0
- package/dist/node/node.d.cts.map +1 -0
- package/dist/node/node.d.ts +236 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +2 -0
- package/dist/node/node.js.map +1 -0
- package/dist/node/project-tool-schema-DrDjGKnH.cjs +1 -0
- package/dist/node/project-tool-schema-_UKfY7yO.js +2 -0
- package/dist/node/project-tool-schema-_UKfY7yO.js.map +1 -0
- package/dist/node/rolldown-runtime-DK3Fl9T5.js +1 -0
- package/dist/node/rolldown-runtime-DWNI8pZO.cjs +1 -0
- package/dist/node/testing/index.cjs +1 -1
- package/dist/node/testing/index.d.cts +96 -0
- package/dist/node/testing/index.d.cts.map +1 -0
- package/dist/node/testing/index.d.ts +35 -4
- package/dist/node/testing/index.d.ts.map +1 -1
- package/dist/node/testing/index.js +1 -1
- package/dist/node/testing/index.js.map +1 -1
- package/dist/node/types-gNrckgFE.d.cts +335 -0
- package/dist/node/types-gNrckgFE.d.cts.map +1 -0
- package/dist/node/types-gNrckgFE.d.ts +335 -0
- package/dist/node/types-gNrckgFE.d.ts.map +1 -0
- package/dist/node/verdict-decoder-Dk_w2NG9.js +2 -0
- package/dist/node/verdict-decoder-Dk_w2NG9.js.map +1 -0
- package/dist/node/verdict-decoder-Estx9MtY.cjs +1 -0
- package/package.json +75 -24
- package/dist/node/index-BKIUt9pk.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,672 @@
|
|
|
1
1
|
# @robota-sdk/agent-core
|
|
2
2
|
|
|
3
|
+
## 3.0.0-beta.81
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3038eb7: A message from another session is instant messaging: text from an untrusted third party that
|
|
8
|
+
carries no authority. What the model does with it is decided by the session's ordinary permissions —
|
|
9
|
+
rules, permission mode and remembered consent — exactly like the session's own work. The per-origin
|
|
10
|
+
peer policy is gone.
|
|
11
|
+
|
|
12
|
+
**BREAKING**
|
|
13
|
+
|
|
14
|
+
- `agent-core`: `IPermissionEvaluationContext.peerTurn` is now a boolean. It decides only whether a
|
|
15
|
+
`repliesToPeer` tool exists; every other call in a peer turn is decided as in any turn. Removed:
|
|
16
|
+
`isToolAvailableInPeerTurn`, `isSecretPath`, `IPeerTurnAuthority`, `TPeerReach` (now exported by
|
|
17
|
+
`agent-interface-session-mobility`) and `IToolPermissionProfile.workspacePaths`.
|
|
18
|
+
- `agent-tools`: `Read` and `Glob` no longer declare `workspacePaths`.
|
|
19
|
+
- `agent-session`: `ISessionRunOptions.peerReach` is replaced by `peerTurn?: boolean`, and
|
|
20
|
+
`ISessionOptions.allowPeerChanges` is removed. An ask in a peer turn is answered like any other:
|
|
21
|
+
a consent the operator remembered answers it, and an "always allow" given there is remembered.
|
|
22
|
+
- `agent-interface-session`: `IPeerTurnContext` no longer has `reach`; it carries only the reply
|
|
23
|
+
route.
|
|
24
|
+
- `agent-interface-session-mobility`: `peerReachOf` is removed; `TPeerReach` moves here. A delegated
|
|
25
|
+
turn carries no reach.
|
|
26
|
+
- `agent-framework`: the `peers.allowChanges` setting is removed (an existing value is ignored). A
|
|
27
|
+
peer turn is offered the ordinary tools, plus `peer_reply`.
|
|
28
|
+
|
|
29
|
+
**Changes**
|
|
30
|
+
|
|
31
|
+
- `agent-framework`: the per-turn statement tells the model the message is an opinion from an
|
|
32
|
+
untrusted third party, not its owner's instruction, and that it decides for itself whether and how
|
|
33
|
+
to act. A message still expands no `@path` and attaches no context reference, and its requests
|
|
34
|
+
still carry no provider-hosted tool, since no permission step can decide one. The session takes
|
|
35
|
+
at most 6 messages a minute and 30 an hour from each sender for a turn; a message over the limit
|
|
36
|
+
is refused with a reason the sender receives. A prompt answer given in the name of a `peer:` or `external:`
|
|
37
|
+
driver is ignored. External-event turns keep their tool-less baseline.
|
|
38
|
+
- `agent-cli`: incoming peer turns carry only their reply route.
|
|
39
|
+
|
|
40
|
+
- 02b7452: A reply to a peer session is decided by the permission system like any call that sends something
|
|
41
|
+
off this machine, and it goes only to the sender that was admitted.
|
|
42
|
+
|
|
43
|
+
- `agent-core` — a tool declaring `repliesToPeer` is still refused outside a peer turn; inside one it
|
|
44
|
+
is decided by the ordinary steps: deny, ask and allow rules, then the mode. It declares no risk
|
|
45
|
+
class, so it asks by default, is refused in plan mode and proceeds under bypass. `IPeerTurnAuthority`
|
|
46
|
+
no longer has `toolUsed`.
|
|
47
|
+
- `agent-session` — an "always allow" answer to the reply is remembered and answers later replies,
|
|
48
|
+
as for any tool; other asks in a peer turn still need a fresh approval each time.
|
|
49
|
+
- `agent-framework` — `peer_reply` asks the operator by default, showing the full text and the peer
|
|
50
|
+
it goes to; `permissions.allow`/`deny` rules naming `peer_reply` apply. `PeerMessageIngress`
|
|
51
|
+
refuses a message whose origin names a sender other than the admitted one, or whose admission names
|
|
52
|
+
no sender, and submits the turn with the admitted identity.
|
|
53
|
+
- `agent-cli` — a local peer message is taken as coming from the session it names only when that
|
|
54
|
+
session confirms, at its own socket, that it is sending exactly that message to this receiver.
|
|
55
|
+
The reply target, the driver id and the operator's notices come from the confirmed sender. A
|
|
56
|
+
session on an earlier version cannot confirm, so its messages are refused; both sessions need this
|
|
57
|
+
version to message each other.
|
|
58
|
+
|
|
59
|
+
- ec5e477: Add a credential store port (`ICredentialStore` in `agent-core`) and keep the CLI's secrets behind it: the OS keychain through the optional `@napi-rs/keyring` binding (macOS Keychain, Windows Credential Manager, Linux Secret Service), else an owner-only file under `~/.robota/credentials`. The backend is chosen at first use, recorded, and named by `/remote-control status`; a recorded keychain that stops working fails closed instead of degrading to the file.
|
|
60
|
+
|
|
61
|
+
The remote-control host identity key moves into the store. The old `~/.robota/remote-host-identity.json` may have been copied by backups or dotfile sync, so it is not carried over: on the first run after upgrading a new host key is generated, the old file is removed, and the operator is told once that trusted devices must pair again.
|
|
62
|
+
|
|
63
|
+
## 3.0.0-beta.80
|
|
64
|
+
|
|
65
|
+
### Major Changes
|
|
66
|
+
|
|
67
|
+
- 4eea54b: **`@robota-sdk/agent-core` removes `IMCPToolConfig` and `IToolFactory.createMCPTool()` with no compatibility facade.**
|
|
68
|
+
|
|
69
|
+
Both were exported with no producer and no consumer. `IToolFactory` had no implementation anywhere in
|
|
70
|
+
this repository — `grep -rn 'IToolFactory' packages/*/src apps/*/src` returned only the declaration
|
|
71
|
+
and its export line — and nothing constructed an `IMCPToolConfig`. An MCP server definition is now
|
|
72
|
+
owned by `@robota-sdk/agent-mcp` (MCP-001), which also absorbs the raw/validated/resolved
|
|
73
|
+
forms, source provenance and shadow metadata, strict foreign `mcpServers` decoding, environment
|
|
74
|
+
templates, whole-entry precedence, reversible disable overlays, redacted management projections and
|
|
75
|
+
activation identity.
|
|
76
|
+
|
|
77
|
+
`major` because an exported type and an interface member are gone. An external implementer of
|
|
78
|
+
`IToolFactory` — there is none in this repository — stops compiling until it removes its
|
|
79
|
+
`createMCPTool` member; a caller of the removed type must take the `agent-mcp` definition contract
|
|
80
|
+
instead. No facade is retained deliberately: keeping a deprecated alias would re-create the state
|
|
81
|
+
this change exists to end, two MCP contracts with one of them dead.
|
|
82
|
+
|
|
83
|
+
`@robota-sdk/agent-mcp` is the same workspace package previously named `@robota-sdk/agent-tool-mcp`,
|
|
84
|
+
renamed in place. It stays `private`, so the rename publishes nothing; MCP-002 owns its publication
|
|
85
|
+
together with the official MCP TypeScript SDK client and the first product-reachable slice.
|
|
86
|
+
|
|
87
|
+
- f336838: One permission evaluation order for every caller. The interactive session, background tasks and
|
|
88
|
+
subagents used to run two different resolvers, so the same call could be decided differently
|
|
89
|
+
depending on who made it. They now share `evaluatePermission`, and a background policy only adds a
|
|
90
|
+
ceiling, an ask-everything flag and the task's own lists to it:
|
|
91
|
+
|
|
92
|
+
deny → caller ceiling → unevaluable deny (ask) → never-auto-approve set (ask) → ask-everything →
|
|
93
|
+
bypassPermissions → allow → mode.
|
|
94
|
+
|
|
95
|
+
- **`ask` rules.** `permissions.ask` patterns always ask, in every mode including
|
|
96
|
+
`bypassPermissions`. They are matched per command like a deny rule, and are validated at
|
|
97
|
+
construction alongside `allow` and `deny`.
|
|
98
|
+
- **Never auto-approved, bypass included:** removing a critical path with `rm`/`rmdir` (the root, a
|
|
99
|
+
top-level directory, home, the working directory or a parent), and a modify-class write into
|
|
100
|
+
`.git`, `.robota`, `.claude`, `.agents`, `.mcp.json`, `.gitconfig`, `.npmrc` or a shell rc file.
|
|
101
|
+
Files inside an isolated worktree (`.robota/worktrees/<name>/…`) are ordinary files. With no
|
|
102
|
+
approver attached, an ask is a denial.
|
|
103
|
+
- **A ceiling is checked before bypass and before any ask.** A subagent's `inherit-allowlist` ceiling
|
|
104
|
+
is now the parent's _effective_ rules, read live at spawn: settings, preset lists and command
|
|
105
|
+
auto-allows. It used to be the raw settings file. An unevaluable deny under a policy now asks,
|
|
106
|
+
like everywhere else, where it used to deny outright; with no approver it is still a denial.
|
|
107
|
+
- **Settings layers union `permissions.allow`**, as they already did `deny`. A checked-in project
|
|
108
|
+
file no longer silently discards the user's allow list.
|
|
109
|
+
- **Print mode, `createQuery()` and headless sessions default to `default` mode**, not
|
|
110
|
+
`bypassPermissions`. They have no approver, so a call that would ask is denied. Pass
|
|
111
|
+
`--permission-mode` / `permissionMode: 'bypassPermissions'` explicitly for unattended runs.
|
|
112
|
+
|
|
113
|
+
**Breaking:**
|
|
114
|
+
- `@robota-sdk/agent-core` removes `resolvePermissionByPolicy` and `TPermissionPolicyDecision` in
|
|
115
|
+
favour of `projectPermissionPolicy` plus `evaluatePermission`'s new `context` argument.
|
|
116
|
+
- `@robota-sdk/agent-framework` changes the settings merge rule for `permissions.allow`, and the
|
|
117
|
+
default permission mode of `createQuery()` and headless sessions.
|
|
118
|
+
|
|
119
|
+
- 475e085: **BREAKING — CORE-028: the Node-only surface moves from the main barrel to `@robota-sdk/agent-core/node`.**
|
|
120
|
+
|
|
121
|
+
`canonicalizePath`, `isPathInside`, `CommandExecutor` and `HttpExecutor` were exported from the main
|
|
122
|
+
barrel. Each one needs `node:fs`, `node:path` or `node:child_process`, so the package's `browser`
|
|
123
|
+
build had those three builtins in its static import graph — a package declaring a `browser` export
|
|
124
|
+
condition while statically importing Node builtins. A bundler resolves that one of two ways: it
|
|
125
|
+
errors, or it aliases the builtin to an empty object and the code fails later at a call site with no
|
|
126
|
+
useful trace. Neither is a build you want to ship.
|
|
127
|
+
|
|
128
|
+
The four now live at the `./node` subpath, which makes the dependency legible at the import site
|
|
129
|
+
instead of hiding it inside a barrel. The main barrel's static graph imports zero Node builtins,
|
|
130
|
+
which a test now holds at zero rather than at a list of known remainders.
|
|
131
|
+
|
|
132
|
+
**Migration** — change the import path; no signature changed:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// before
|
|
136
|
+
import {
|
|
137
|
+
canonicalizePath,
|
|
138
|
+
isPathInside,
|
|
139
|
+
CommandExecutor,
|
|
140
|
+
HttpExecutor,
|
|
141
|
+
} from '@robota-sdk/agent-core';
|
|
142
|
+
|
|
143
|
+
// after
|
|
144
|
+
import {
|
|
145
|
+
canonicalizePath,
|
|
146
|
+
isPathInside,
|
|
147
|
+
CommandExecutor,
|
|
148
|
+
HttpExecutor,
|
|
149
|
+
} from '@robota-sdk/agent-core/node';
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`./node` carries `"browser": null`, declaring that the subpath has no browser implementation, so a
|
|
153
|
+
resolver that honours a null target refuses it by name instead of quietly serving the Node build.
|
|
154
|
+
|
|
155
|
+
That declaration is worth stating precisely, because it was measured rather than assumed. Bundling
|
|
156
|
+
`import '@robota-sdk/agent-core/node'` under `['browser', 'import', 'default']` with the repo's own
|
|
157
|
+
bundler does NOT stop at the null: through a workspace link it resolves the `source` entry and then
|
|
158
|
+
fails on `node:fs`, `node:path` and `node:child_process`. The build still breaks, and the message
|
|
159
|
+
still names the Node dependency, so the failure is visible either way — but by a different mechanism
|
|
160
|
+
than the null, and only spec-compliant resolvers give the cleaner error. The claim here is the
|
|
161
|
+
declaration, not a guarantee about every bundler.
|
|
162
|
+
|
|
163
|
+
### Minor Changes
|
|
164
|
+
|
|
165
|
+
- 1698be4: A child-process subagent can no longer send the parent's provider credential to a different
|
|
166
|
+
endpoint.
|
|
167
|
+
|
|
168
|
+
- **Before spawning a child**, the parent compares every environment variable that decides where
|
|
169
|
+
the provider connects or which credential it sends. If the child's environment differs, the job
|
|
170
|
+
is refused before the credential leaves the parent. The error names the variable, never its
|
|
171
|
+
value. The variables compared are:
|
|
172
|
+
- the proxy and TLS variables;
|
|
173
|
+
- the variables the provider's SDK reads, such as `OPENAI_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` and
|
|
174
|
+
the Vertex settings;
|
|
175
|
+
- the credential's own variable.
|
|
176
|
+
- **The child** repeats the check before it builds its provider. It builds that provider from the
|
|
177
|
+
parent's effective connection exactly: it no longer fills in a base URL, options or a default
|
|
178
|
+
credential from its own registry, and a credential reference that resolves to nothing is refused.
|
|
179
|
+
- **Where the effective connection comes from:** the parent applies its own definition defaults
|
|
180
|
+
(base URL, options). `profileName` is sent only when it names the connection actually sent.
|
|
181
|
+
- **New contracts:**
|
|
182
|
+
- `IProviderDefinition.destinationEnvironment`, declared by every built-in provider.
|
|
183
|
+
- `createProviderFromExactProfile`, `connectionEnvironmentNames`,
|
|
184
|
+
`findConnectionEnvironmentDivergence`, `sealConnectionEnvironment`,
|
|
185
|
+
`verifyConnectionEnvironment` and `TRANSPORT_ENVIRONMENT`.
|
|
186
|
+
- The start payload's `connectionCheck`.
|
|
187
|
+
- The child-process runner's `providerDefinitions` option, now required: a provider with no
|
|
188
|
+
definition there is refused, because its connection cannot be checked.
|
|
189
|
+
|
|
190
|
+
- d4189b9: The rest of the per-server MCP OAuth lifecycle: signing in to a server without a local browser,
|
|
191
|
+
signing out of it with token revocation, and each server's sign-in state in `/mcp`.
|
|
192
|
+
|
|
193
|
+
- **`robota mcp login <name> --no-browser`** prints the authorization URL and reads the redirect URL
|
|
194
|
+
the user pastes back (not echoed). `runMCPOAuthLogin` takes `readRedirect` for this; nothing
|
|
195
|
+
listens on the redirect URI then. The pasted URL is held to the loopback listener's rules through
|
|
196
|
+
`createPastedRedirectAcceptor`: it must be the registered redirect URI (same origin and path, no
|
|
197
|
+
user info), carry the sign-in's `state` (compared in constant time), and is accepted once; an
|
|
198
|
+
error redirect is `authorization-denied` without its `error_description`, and the RFC 9207 `iss`
|
|
199
|
+
check applies as before. `openBrowser` now also receives the redirect URI. `readRedirect` gets a
|
|
200
|
+
signal that aborts on cancel or at `callbackTimeoutMs` (5 minutes by default, as for the loopback
|
|
201
|
+
listener); a paste longer than the prompt accepts fails as `redirect-too-long`.
|
|
202
|
+
- **Signing out** (`runMCPOAuthLogout`; `robota mcp logout <name>`; `/mcp logout <serverId>`):
|
|
203
|
+
deletes the stored credential under the refresh lock, then revokes the refresh token and the
|
|
204
|
+
access token (RFC 7009) when the stored issuer advertises `revocation_endpoint`. Revocation is a
|
|
205
|
+
POST through the egress policy that never follows a redirect and is byte-bounded; it authenticates
|
|
206
|
+
the client by `revocation_endpoint_auth_methods_supported` when advertised, otherwise the way its
|
|
207
|
+
refresh does. The local delete always happens. Each token's outcome is reported in `tokens`, and
|
|
208
|
+
overall as `revoked`, `partial`, `unsupported`, `failed` or `not-attempted`, with fixed reasons
|
|
209
|
+
only (new: `revocation-failed`, and `token-type-not-revocable` for RFC 7009
|
|
210
|
+
`unsupported_token_type`). `/mcp logout` also makes the session's authenticator drop the token it
|
|
211
|
+
holds (`IMCPOAuthAuthenticator.forget`).
|
|
212
|
+
- **Sign-in state:** `readMCPOAuthCredentialState` answers `signed-in`, `expired-refreshable`,
|
|
213
|
+
`sign-in-required` or `signed-out` — never anything token-derived. `/mcp` shows it for every
|
|
214
|
+
OAuth server; a server this session was told needs a sign-in reads `sign-in-required`.
|
|
215
|
+
`ICommandMCPActivationAdapter` gains optional `oauthStatus` and `oauthLogout`
|
|
216
|
+
(`ICommandMCPOAuthStatus`, `ICommandMCPOAuthLogoutResult`).
|
|
217
|
+
- **No in-session sign-in:** signing in stays `robota mcp login <server>` in a terminal, since it
|
|
218
|
+
needs the terminal (browser, pasted redirect, hidden secret prompt) a running session owns. `/mcp`
|
|
219
|
+
names that command for a server that needs a sign-in, as does the session's sign-in notice; the
|
|
220
|
+
server name is shown there only when it is safe to paste into any shell — a plain token not
|
|
221
|
+
starting with `-` or `=` — and is otherwise replaced by `<server>` (`shellArgumentForDisplay` in
|
|
222
|
+
`agent-core`; nothing is quoted, since quoting rules differ between shells). Server names in OAuth
|
|
223
|
+
notices have control and format characters escaped.
|
|
224
|
+
- **Refresh hardening:** on `invalid_grant`, the store is read again under the lock; a refresh token
|
|
225
|
+
another holder rotated in meanwhile is kept (and used when still valid) instead of being deleted.
|
|
226
|
+
The expiry skew is capped at half the token's lifetime, so a short-lived token is not refreshed on
|
|
227
|
+
every request; credentials now record `issuedAt` for this.
|
|
228
|
+
|
|
229
|
+
- 9edae52: Remote MCP servers can authenticate with OAuth: `robota mcp login <name>` signs in, and sessions
|
|
230
|
+
send the stored token.
|
|
231
|
+
|
|
232
|
+
- **Declaring it:** `"oauth": { "clientId"?, "callbackPort"?, "authServerMetadataUrl"?, "scopes"? }`
|
|
233
|
+
on a remote server definition, decoded into `IMCPOAuthConfig`.
|
|
234
|
+
- `authServerMetadataUrl` must be `https`. `scopes` wins over the server's metadata and a 401's
|
|
235
|
+
`scope`.
|
|
236
|
+
- A `clientId` needs a `callbackPort`: a pre-registered client's redirect URI is fixed.
|
|
237
|
+
- `clientSecret`, unknown keys and `${}` templates are refused; `oauth` beside `headersHelper`,
|
|
238
|
+
or on a stdio server, is refused.
|
|
239
|
+
- `oauth` is no longer reported as unsupported authentication, and it is part of the definition
|
|
240
|
+
fingerprint.
|
|
241
|
+
- **Signing in** (`runMCPOAuthLogin`; `robota mcp login <name> [--client-secret]`):
|
|
242
|
+
- Discovery is done here, not by the SDK's `discoverOAuthServerInfo`: the protected-resource
|
|
243
|
+
metadata `resource` must be the canonical server URL, the authorization server's `issuer` must
|
|
244
|
+
be the server it was fetched for, the server, authorization, token and registration
|
|
245
|
+
endpoints must be `https`, and the authorization server must advertise PKCE `S256`
|
|
246
|
+
(`pkce-unsupported` otherwise). A 401's `resource_metadata` is never followed to another origin,
|
|
247
|
+
and a server without resource metadata is not guessed at.
|
|
248
|
+
`authServerMetadataUrl` skips resource discovery.
|
|
249
|
+
- Dynamic client registration when there is no `clientId`; PKCE, a random `state` and the
|
|
250
|
+
RFC 8707 resource indicator on the authorization request; the RFC 9207 `iss` checked on the
|
|
251
|
+
redirect when sent, and required when the server promises it.
|
|
252
|
+
- The loopback callback listens on `127.0.0.1`, checks `Host`, answers only `GET /callback` with
|
|
253
|
+
the sign-in's `state` (compared in constant time), once, within 5 minutes, and serves a static
|
|
254
|
+
page that never shows `error_description`.
|
|
255
|
+
- The browser opens by argv (`open`, `xdg-open`, `rundll32 url.dll,FileProtocolHandler`) and only
|
|
256
|
+
for an `https` URL. `--client-secret` asks for the secret without echo (or reads stdin's first
|
|
257
|
+
line) and only for a pre-registered client.
|
|
258
|
+
- **Network:** every OAuth request goes through the egress policy and is capped at 256 KiB.
|
|
259
|
+
`agent-core` adds `postWithEgressPolicy`, which refuses a redirect (`redirect_refused`) instead
|
|
260
|
+
of following it; registration, token and refresh requests use it.
|
|
261
|
+
- **Storage:** `IMCPOAuthCredentialStore` (`get`/`set`/`delete`), keyed by the server's security
|
|
262
|
+
identity and canonical URL together. `createFileOAuthCredentialStore` keeps 0600 files in a 0700
|
|
263
|
+
`~/.robota/mcp-credentials/`. The issuer, token endpoint and client (with its secret, if any, and
|
|
264
|
+
the client authentication method dynamic registration returned) are stored with the tokens. A
|
|
265
|
+
sign-in stores its credential under the refresh lock, so a refresh in flight cannot overwrite it.
|
|
266
|
+
- **Sessions** (`createOAuthAuthenticator`, wired for every `oauth` definition):
|
|
267
|
+
- Sends the stored token as `Authorization: Bearer`, refreshing it first when expired — including
|
|
268
|
+
a token cached earlier in the session — and only at the stored token endpoint.
|
|
269
|
+
- One refresh at a time per credential: in a process through `MCPSingleFlightCache`, across
|
|
270
|
+
processes under `createFileOAuthRefreshLock`, re-reading the store once the lock is held. A
|
|
271
|
+
stale lock is taken over, and a lock released, only after it is renamed aside and proven to be
|
|
272
|
+
the one judged — never another process's fresh lock.
|
|
273
|
+
- A 401 rediscovers, then refreshes once and retries; an authorization server that changed clears
|
|
274
|
+
the tokens — compared, under the lock, with what is stored then, so a newer sign-in is kept. `invalid_grant`, or nothing stored, asks the user to run `robota mcp login <name>`. A 403
|
|
275
|
+
`insufficient_scope` fails and names the scope.
|
|
276
|
+
- An `oauth` definition is never connected without the authenticator (`oauth-unavailable`).
|
|
277
|
+
- Failures are `MCPOAuthError` with a fixed reason; no code, verifier, token, secret or
|
|
278
|
+
authorization-server text reaches an error, a notice or a log.
|
|
279
|
+
|
|
280
|
+
- 4078a72: Model fallback chain. `--fallback-model a,b` (or the `fallbackModel` settings array; the flag wins) names up to three models a turn moves to when its model is overloaded, unavailable or failing on the server. An entry is a provider profile, `profile:model`, a bare model on the primary's provider, or `default`. The move happens only before any output has streamed, lasts for the current turn, and is shown as a system note; entries that cannot be built are passed over and entries outside the organization's `allowedProviders` are dropped with a notice. `FallbackProvider` in agent-framework implements it over the session's provider; `IChatOptions` gains `executionId`, `onModelFallback` and `preserveContextWindow`, `IAIProvider` gains optional `resolveModelRoute`, and the execution loop emits a `provider_fallback` event and attributes requests, call observations, committed replies, the response cache and usage to the model that answered. A turn that ran on more than one model records per-model `modelShares` on its usage observation, which personal usage reports split by model and provider. A `/provider` switch keeps the chain, read again for the new primary.
|
|
281
|
+
- 4c73a0b: Provider failures keep the vendor's HTTP status and error type. `ProviderError` gains `status` and `type`, adapters throw `ProviderError` (or `RateLimitError` for a rate limit) instead of a bare `Error`, an Anthropic mid-stream `overloaded_error` event surfaces as a `ProviderError` with that type, and media errors carry `status`. New `classifyProviderFailure` says whether a failure is worth retrying on another model, and `toProviderError` is the shared adapter mapping.
|
|
282
|
+
- 196a900: Permission rules can say more than one argument per tool.
|
|
283
|
+
|
|
284
|
+
- **`Tool(name:value)` in deny and ask rules** matches a named top-level parameter, with `*` in the
|
|
285
|
+
value (`Bash(run_in_background:true)`, `Agent(model:opus*)`, `github__create_issue(repo:acme/*)`).
|
|
286
|
+
It is a parameter rule only when `name` is one of the tool's parameters, so
|
|
287
|
+
`WebFetch(https://…)` keeps its meaning. A parameter the call omits never matches, and a
|
|
288
|
+
non-scalar value is unevaluable, so the call asks. Allow rules may not use the form.
|
|
289
|
+
- **A rule on the primary field** (`Bash(command:rm *)`) is reported at startup and asks on every
|
|
290
|
+
call, instead of being silently ignored.
|
|
291
|
+
- **Tool-name globs.** Deny and ask rules may glob the tool name (`github__*`). Allow rules may do so
|
|
292
|
+
only after a literal `<server>__` prefix; an unanchored allow glob is refused at construction.
|
|
293
|
+
- **A bare-name deny removes the tool from the model's context.** `Tool`, `Tool(*)` or a name glob
|
|
294
|
+
withholds it from the offered set and the deferred-tool catalogue, live, instead of offering it
|
|
295
|
+
and refusing every call. `IAgentConfig.isToolVisible` is the new seam.
|
|
296
|
+
- **MCP canonical names keep the whole `<server>__` prefix when truncated**, so a server glob still
|
|
297
|
+
names every tool of that server.
|
|
298
|
+
|
|
299
|
+
- 34e50f0: A new permission mode, `auto`, lets a model classifier approve or block what would otherwise prompt.
|
|
300
|
+
|
|
301
|
+
- **What it decides:**
|
|
302
|
+
- Reads and in-workspace edits run as in `acceptEdits`.
|
|
303
|
+
- Commands and other calls the mode leaves open go to the classifier, a side call to the
|
|
304
|
+
session's own model. It sees the call, the working directory and the git remotes, never the
|
|
305
|
+
conversation.
|
|
306
|
+
- A block reaches the model with its reason, so it can take another route.
|
|
307
|
+
- **What still reaches a person, or is refused:**
|
|
308
|
+
- Deny rules and background ceilings apply first.
|
|
309
|
+
- `ask` rules, critical removals and protected paths ask a person.
|
|
310
|
+
- After 3 refusals in a row (blocks, or no usable verdict), or 20 blocks in the session, the
|
|
311
|
+
mode asks a person until one approves.
|
|
312
|
+
With no one to ask, the call is denied.
|
|
313
|
+
- **Allow rules:** in `auto` mode, allow rules that approve any command are set aside while the
|
|
314
|
+
mode is on. Examples are `Bash(*)`, an interpreter (`Bash(python *)`), a package runner
|
|
315
|
+
(`Bash(npm run *)`, `Bash(pnpm exec *)`), `Agent`, `ExecuteCommand` or `Computer`. Narrow
|
|
316
|
+
rules still apply.
|
|
317
|
+
- **Retry:** `/permissions` lists classifier blocks. `/permissions retry <n>` lets that exact call
|
|
318
|
+
run once, unjudged, when the model tries it again.
|
|
319
|
+
- **Turning it on and off:**
|
|
320
|
+
- `--permission-mode auto`, `/mode auto` or `/permissions auto`.
|
|
321
|
+
- An organization turns it off with `disableAutoMode` in the org policy.
|
|
322
|
+
- **New contracts:**
|
|
323
|
+
- `TPermissionMode` gains `'auto'`.
|
|
324
|
+
- `allowRulesForAutoMode` and `isBroadExecutionAllowRule`.
|
|
325
|
+
- `IPermissionClassifier` and `AutoModeGate`.
|
|
326
|
+
- The `permissionClassifier` session option.
|
|
327
|
+
- `Session.retryPermissionDenial`, and `retryDenial` on the permission-mode adapter.
|
|
328
|
+
- The `'classifier'` denial reason.
|
|
329
|
+
- `createModelPermissionClassifier`.
|
|
330
|
+
- The `disableAutoMode` option on `createSession` and `IOrgPolicy`.
|
|
331
|
+
|
|
332
|
+
- 722e88a: Shell commands can run in an OS-level sandbox: bubblewrap on Linux and WSL2, Seatbelt on macOS.
|
|
333
|
+
|
|
334
|
+
- **Confinement:** covers the command and every process it starts.
|
|
335
|
+
- Writes are limited to the working directory, the temporary directories and
|
|
336
|
+
`sandbox.filesystem.allowWrite`.
|
|
337
|
+
- Agent, git-hook, MCP and shell configuration inside the workspace stays read-only.
|
|
338
|
+
- `sandbox.filesystem.denyRead` hides paths from the command.
|
|
339
|
+
- The network is on or off (`sandbox.network.enabled`).
|
|
340
|
+
- **Modes:** `/sandbox` switches between `auto-allow`, `regular` and `off` for the next command and
|
|
341
|
+
saves the choice.
|
|
342
|
+
- In `auto-allow` (`sandbox.autoAllowBashIfSandboxed`), a confined command runs without a prompt
|
|
343
|
+
in `default` and `acceptEdits`.
|
|
344
|
+
- Deny rules, ask rules, critical removals and plan mode still apply first.
|
|
345
|
+
- **Exclusions:** `sandbox.excludedCommands` run unconfined, through the ordinary permission path.
|
|
346
|
+
- **When the sandbox cannot run:** a missing or unusable backend is reported at startup, in
|
|
347
|
+
`robota doctor` and in `/sandbox`, and commands then run unconfined.
|
|
348
|
+
`sandbox.failIfUnavailable` refuses to start instead.
|
|
349
|
+
- **New contracts:**
|
|
350
|
+
- `OsSandboxClient`, `detectOsSandbox`, `bubblewrapArguments`, `seatbeltProfile`.
|
|
351
|
+
- `ISandboxClient.wrapCommand` and `autoApproves`.
|
|
352
|
+
- `IPermissionEvaluationContext.sandboxAutoApproved`.
|
|
353
|
+
- The `commandSandbox` session option.
|
|
354
|
+
- The `sandbox` settings key and the `sandbox` command host adapter.
|
|
355
|
+
|
|
356
|
+
- d23c848: Built-in read-only shell commands run without a prompt.
|
|
357
|
+
|
|
358
|
+
A Bash call is decided like a read, and runs without a prompt in every mode (`plan` included), when
|
|
359
|
+
every command in it comes from the fixed read-only set (`ls`, `cat`, `grep`, `find` without actions,
|
|
360
|
+
`git status`/`log`/`diff`/`show` and similar) and it also:
|
|
361
|
+
|
|
362
|
+
- stays inside the workspace: every path operand resolves inside once symlinks are followed, as
|
|
363
|
+
`Read` requires;
|
|
364
|
+
- uses only printable ASCII syntax that bash, zsh, fish and PowerShell all read the same way.
|
|
365
|
+
|
|
366
|
+
Deny and ask rules still apply first. A command that writes through a redirect, expands or
|
|
367
|
+
substitutes anything, or runs git outside the session's repository takes the ordinary path.
|
|
368
|
+
|
|
369
|
+
New exports: `isReadOnlyCommandLine` and `TResolveInWorkspace`. `IPermissionEvaluationContext`
|
|
370
|
+
gains `resolveInWorkspace`, which `PermissionEnforcer` supplies.
|
|
371
|
+
|
|
372
|
+
- fec722f: Carry a trusted canonical absolute execution root from DAG product composition through worker task
|
|
373
|
+
input and node lifecycle context. Filesystem-capable DAG nodes now use that injected authority instead
|
|
374
|
+
of ambient `process.cwd()`, and authored `cwd` values may only narrow it.
|
|
375
|
+
|
|
376
|
+
BREAKING: `ITaskExecutionInput`, `INodeExecutionContext`, worker composition dependencies,
|
|
377
|
+
`LocalDagRuntimeProvider`, and the CLI-local runner now require an execution root at their non-convenience
|
|
378
|
+
boundaries. `createDagFramework()` preserves no-argument construction by validating and capturing its
|
|
379
|
+
current directory at the factory boundary. The filesystem-backed skill node is explicitly Node-only and
|
|
380
|
+
no longer advertises a browser export condition.
|
|
381
|
+
|
|
382
|
+
- 2d3b2c0: Make shell resolution executable-aware across managed and scheduled background command runners. The core
|
|
383
|
+
resolver now accepts one request with explicit-executable precedence, returns matching argument families
|
|
384
|
+
for sh/bash, PowerShell/pwsh, and cmd, and fails closed with `UnsupportedShellError` for unknown explicit
|
|
385
|
+
executables. Executor exposes one shared request adapter and both concrete runners consume its pair.
|
|
386
|
+
- 4772067: **BREAKING — ARCH-031: the subagent seam is derived from its transport SSOT instead of copied.**
|
|
387
|
+
|
|
388
|
+
One field family — what a subagent job IS — was declared three times as independent shapes and carried
|
|
389
|
+
between them by six hand-written object literals that nothing checked for totality. A field added to
|
|
390
|
+
either side had to be hand-copied at every hop, and a miss compiled clean as a silent no-op. TYPE-003
|
|
391
|
+
named this cause and derived one hop; the next two changes each dropped a field at a hop it had skipped
|
|
392
|
+
(CORE-025's permission policy, and ANALYTICS-001's `usage`, dropped in the very commit that added it).
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
// now, in @robota-sdk/agent-interface-transport
|
|
396
|
+
export type ISubagentSpawnRequest = Omit<IAgentBackgroundTaskRequest, 'kind'>;
|
|
397
|
+
export type ISubagentJobResult = Omit<IBackgroundTaskResult, 'kind' | 'exitCode' | 'signalCode'>;
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
All four projections collapse to spreads. `parentTaskId` and `providerProfile` now reach the runner
|
|
401
|
+
because they exist on the source, not because someone remembered them.
|
|
402
|
+
|
|
403
|
+
**Per package, classified against each barrel:**
|
|
404
|
+
|
|
405
|
+
- **`agent-executor` (major)** — the barrel loses `ISubagentSpawnRequest` and `ISubagentJobResult` (they
|
|
406
|
+
moved to their owner; re-publishing them here would be a pass-through re-export). `ISubagentJobStart`
|
|
407
|
+
and `ISubagentJobHandle` rename `jobId` → `taskId`, `ISubagentJobStart` gains `worktree?`, and
|
|
408
|
+
`ISubagentWorktreePrepareRequest` renames `jobId` → `taskId`.
|
|
409
|
+
- **`agent-framework` (major)** — the barrel loses eleven type-only re-exports of `agent-executor`-owned
|
|
410
|
+
types. They carried zero runtime values, so they bought none of the assembly convenience a runtime
|
|
411
|
+
facade exists for, while making one field family look like it had three owners. Separately,
|
|
412
|
+
`ISpawnAgentTaskRequest.permissionPolicy` goes optional → **required**.
|
|
413
|
+
- **`agent-subagent-runner` (major)** — `ISubagentWorkerStartPayload` renames `jobId` → `taskId` and gains
|
|
414
|
+
`worktree?`. This package is not in the item's declared `area:`; the audit that caught it is the reason
|
|
415
|
+
it is here.
|
|
416
|
+
- **`agent-interface-transport` (minor)** — two new barrel exports; nothing removed or renamed.
|
|
417
|
+
- **`agent-core` (minor)** — **two** new barrel exports. `DEFAULT_BACKGROUND_PERMISSION_POLICY` is the
|
|
418
|
+
intended one; collapsing the hand-listed permissions block to `export *` also surfaced
|
|
419
|
+
`clearRegisteredToolArgumentKeys`, which the old list had omitted. It is documented as public rather
|
|
420
|
+
than re-narrowed — a barrel that cannot fall out of step with its owner is the point of the collapse.
|
|
421
|
+
Nothing was removed: all nine previously-listed permission types remain on the barrel.
|
|
422
|
+
- **`agent-cli` (patch)** — migrated as the only in-repo implementer of `ISubagentWorktreeAdapter`; no
|
|
423
|
+
barrel change.
|
|
424
|
+
|
|
425
|
+
**`permissionPolicy` is now required at the spawn boundary**, and its default is one exported constant
|
|
426
|
+
owned by the permission SSOT. It was previously applied as `?? 'inherit-allowlist'` in the middle of a
|
|
427
|
+
projection, in **two** packages independently, with nothing keeping them equal — a security-relevant value
|
|
428
|
+
whose default was declared twice. Every spawn site now states its own policy.
|
|
429
|
+
|
|
430
|
+
**The worktree identity moved to the runner envelope.** It is runner-produced — the worktree does not
|
|
431
|
+
exist when a caller builds a request — so it rides on `ISubagentJobStart.worktree` and crosses the IPC
|
|
432
|
+
boundary there. The runner no longer also rewrites `request.cwd`, which had given ARCH-010's execution-root
|
|
433
|
+
rule two carriers that could disagree. `branchName` **relocated rather than being deleted**: it has no
|
|
434
|
+
reader in this repository today, and for a library that is not a reason to drop a legitimate contract.
|
|
435
|
+
|
|
436
|
+
**Renames are consistent across the SPI** (`type` → `agentType`, `jobId` → `taskId`) rather than applied to
|
|
437
|
+
one shape, which would have left two names for one identifier in a single file. The IPC validator's
|
|
438
|
+
string-literal keys are now typed against the contract, so the next rename is a compile error instead of a
|
|
439
|
+
runtime rejection of every start payload.
|
|
440
|
+
|
|
441
|
+
- 9fbab1b: Provider DIP Stage B (ARCH-PROVIDER-003), part 1: collapse infrastructure. Adds the
|
|
442
|
+
provider-registry-driven `@robota-sdk/dag-node-llm-text` node that supersedes the
|
|
443
|
+
per-vendor LLM nodes + router, relocates the provider config resolver into
|
|
444
|
+
`agent-core`, adds SSOT cost/allowedModels fields, inverts the `llm-text` validator
|
|
445
|
+
tombstone, and wires `createDagFramework({ providers })`. Additive — the per-vendor
|
|
446
|
+
nodes still exist; consumer migration + their removal follow in part 2.
|
|
447
|
+
- b6d14ce: Make the universal JSON-schema subset able to express an object, so a nested `z.object()` keeps its
|
|
448
|
+
properties and required fields instead of reaching the model as `{ "type": "object" }`. Tools and
|
|
449
|
+
structured-output schemas with one level of nesting are now advertised in full and enforced on the
|
|
450
|
+
tool-input path; `z.union` / `z.discriminatedUnion` / `z.literal` are supported and map to `anyOf`
|
|
451
|
+
and single-value enums; `.nullable()` keeps its null branch; and Zod's `strip`, `strict` and
|
|
452
|
+
`passthrough` modes stop collapsing into two `additionalProperties` emissions. Fixes the shipped
|
|
453
|
+
`Computer` and `AskUserQuestion` built-ins, whose action and question fields were being dropped
|
|
454
|
+
entirely.
|
|
455
|
+
|
|
456
|
+
`agent-core` is **minor**, not patch: `IObjectParameterSchema` is a new export, and
|
|
457
|
+
`IParameterSchema.type` became optional (a union node carries `anyOf` instead of a type), which is a
|
|
458
|
+
consumer-visible type change — two call sites in this repo needed editing to keep compiling.
|
|
459
|
+
`additionalProperties` also widened to `boolean | IParameterSchema`, and `required` and `anyOf` are
|
|
460
|
+
new members. The provider packages are **patch**: each adapts to the widened subset without changing
|
|
461
|
+
its own public surface.
|
|
462
|
+
|
|
463
|
+
### Patch Changes
|
|
464
|
+
|
|
465
|
+
- 7b6234c: A tool call whose arguments fail to decode to a JSON object (invalid JSON, or a `null`/scalar/array
|
|
466
|
+
root — including a stream truncated mid-argument) no longer breaks the rest of its batch or the round
|
|
467
|
+
after it. Every other call in the same batch still executes and gets its real result; the malformed
|
|
468
|
+
call gets a clear per-call error instead, naming the tool and call id, and the run continues rather
|
|
469
|
+
than rejecting. The Anthropic and Gemini providers no longer throw when building the next request
|
|
470
|
+
from a conversation that still carries that call's original malformed arguments.
|
|
471
|
+
- a009f5b: Provider DIP Stage E (ARCH-PROVIDER-006): repo-hygiene + policy cleanup. Codified the
|
|
472
|
+
Family Decomposition Rule in project-structure.md (split driver = consumer/third-party
|
|
473
|
+
opt-in installability / extension-point registration, not dep-weight). Removed dangling
|
|
474
|
+
tracked references to deleted packages (tsconfig project refs, eslint glob, changeset
|
|
475
|
+
config `fixed` + pending changeset files), pruned 37 dead `.changeset/pre.json` entries,
|
|
476
|
+
and removed 24 untracked husk directories. Closes ARL-15 — the provider dependency-
|
|
477
|
+
inversion arc (Stages A–E) is complete.
|
|
478
|
+
- 4f3c075: Assemble complete, verified package generations before switching build output; preserve the previous generation on build failure and pack only verified regular-file images. Include copied CLI web assets in affected-build ordering and artifact transfer. Preserve the CLI version in managed build paths. Public runtime contracts remain compatible (patch).
|
|
479
|
+
- e477440: CORE-033: the abnormal paths now emit the required replay events, and history stays append-only
|
|
480
|
+
|
|
481
|
+
`provider_request`, `assistant_message_committed` and `history_mutation` are REQUIRED families, and
|
|
482
|
+
`agent-session` builds its session log from them: replaying every announced append, in order, is
|
|
483
|
+
supposed to reconstruct the conversation. Three engine sites appended without announcing, so the
|
|
484
|
+
reconstruction diverged at exactly the moments a reader goes to the log — the round cap, the
|
|
485
|
+
hard-capacity block, and a provider failure. On a capped run the summary the user actually read was
|
|
486
|
+
absent from every replay, and the provider call that produced it was invisible.
|
|
487
|
+
|
|
488
|
+
- The forced-summary call emits `provider_request` (`forcedSummary: true`, carrying the assembled
|
|
489
|
+
messages), then `assistant_message_committed` and `history_mutation` for the summary it commits.
|
|
490
|
+
- The hard-capacity block announces its diagnostic — the only message explaining why the turn stopped.
|
|
491
|
+
- A provider failure announces the `Request failed: …` record it appends.
|
|
492
|
+
|
|
493
|
+
The forced-summary call also **rewrote** history: it appended a synthetic round-limit instruction,
|
|
494
|
+
sent it, then removed it with `clear()` + re-add — a non-append mutation this vocabulary has no way
|
|
495
|
+
to describe. The instruction is a per-call prompt artifact, so it no longer enters the conversation
|
|
496
|
+
store at all; it exists only in the outgoing array, the same shape the structured-output transport
|
|
497
|
+
uses for a schema instruction. Nothing is added, so nothing has to be removed, and `mutation` still
|
|
498
|
+
needs no removal member.
|
|
499
|
+
|
|
500
|
+
The item also reported that the streaming path emitted no families at all. CORE-042 had already
|
|
501
|
+
fixed that by removing the second engine — `runStream` runs the same `execute()` — and this change
|
|
502
|
+
adds the test that says so rather than leaving it assumed.
|
|
503
|
+
|
|
504
|
+
- 9dcb5da: CORE-035: the identical-tool-input loop guard throws a named error, and the SPEC now matches it
|
|
505
|
+
|
|
506
|
+
The SPEC documented an `AbortError` thrown at the Nth identical call. The code threw a bare `Error`
|
|
507
|
+
at the N+1th. The type difference was behavioral, not cosmetic: `isAbortFailure` resolves an
|
|
508
|
+
`AbortError` as `success: true, interrupted: true`, so a run that detected a pathological loop and
|
|
509
|
+
produced no answer would have been reported as a SUCCESS.
|
|
510
|
+
|
|
511
|
+
Resolved toward the code's semantics and the SPEC's intent, which are not the same thing:
|
|
512
|
+
|
|
513
|
+
- **Failure, not abort.** A guard trip is the agent giving up, not the user cancelling. `AbortError`
|
|
514
|
+
means the caller asked the turn to stop, and here nobody did.
|
|
515
|
+
- **Named, not bare.** `SameToolInputLoopError` (`code: 'SAME_TOOL_INPUT_LOOP'`, `category: 'system'`,
|
|
516
|
+
`recoverable: true`) carries `toolName`, `callCount` and `maxSameToolInputs`. Naming a type is what
|
|
517
|
+
the SPEC was reaching for: a caller must be able to tell "the agent looped" from "the network died",
|
|
518
|
+
and CORE-027 carries those fields out intact.
|
|
519
|
+
- **`maxSameToolInputs` is a MAXIMUM.** The Nth identical call is allowed; the N+1th trips. The SPEC's
|
|
520
|
+
"N or more times" contradicted its own option name.
|
|
521
|
+
|
|
522
|
+
`ErrorUtils` moves to `utils/error-utils.ts`, split from the class taxonomy it operates on — two
|
|
523
|
+
responsibilities in one file, and keeping them together made the taxonomy file impossible to extend.
|
|
524
|
+
Both remain exported from the package entry.
|
|
525
|
+
|
|
526
|
+
- a95ca85: Apply `config.systemMessage` on the streaming path. `runStream()` built its provider request straight
|
|
527
|
+
from the conversation store and never entered the session initialization that attaches the system
|
|
528
|
+
prompt, so an agent obeyed its persona through `run()` and ignored it through `runStream()` — silently,
|
|
529
|
+
on the default interactive surface. An agent used through both entry points only acquired its prompt
|
|
530
|
+
from the first non-streaming turn onward.
|
|
531
|
+
|
|
532
|
+
The streaming path now enters the same `initializeConversationStore` the round path enters, so the
|
|
533
|
+
prompt, the inject-once rule and the conversation restore are owned in one place. Contained under
|
|
534
|
+
CORE-042, which is the duplication itself.
|
|
535
|
+
|
|
536
|
+
- 0382a51: CORE-041: `z.nativeEnum()` and `z.date()` convert; the rest of the boundary is published, not discovered
|
|
537
|
+
|
|
538
|
+
`zodToJsonSchema` threw `Unsupported Zod type: …` on five constructs, so a schema Zod accepts could
|
|
539
|
+
not be turned into a tool or a structured-output spec. Re-running the decision the item reserved,
|
|
540
|
+
those five are two different problems:
|
|
541
|
+
|
|
542
|
+
- **`ZodNativeEnum` and `ZodDate` are exactly expressible** and were missing for no reason.
|
|
543
|
+
A native enum becomes an `enum` of its VALUES — for a numeric TypeScript enum the compiler's
|
|
544
|
+
reverse mapping is filtered out, so a field accepting `0` is no longer advertised as accepting
|
|
545
|
+
`"Low"`. A date becomes `{ type: 'string', format: 'date-time' }`, which is not a lossy stand-in:
|
|
546
|
+
JSON has no date type, so a string is what the provider receives either way.
|
|
547
|
+
- **`ZodTuple`, `ZodIntersection` and `ZodLazy` are not expressible**, and still throw. A tuple needs
|
|
548
|
+
positional `items` (the subset models `items` as one schema), an intersection needs `allOf`,
|
|
549
|
+
recursion needs `$ref` — none of which the field-enumerated provider mappers would forward.
|
|
550
|
+
Adopting `zod-to-json-schema` does not dissolve this, as CORE-039 had conceded it might: the
|
|
551
|
+
library emits exactly those constructs. The difficulty was never parsing Zod; the target language
|
|
552
|
+
cannot say these things.
|
|
553
|
+
|
|
554
|
+
Mapping them lossily was rejected — a tuple flattened to `array of anyOf[...]` would tell the model
|
|
555
|
+
that any order and any length are acceptable, a contract the author did not write.
|
|
556
|
+
|
|
557
|
+
The error now names the construct, says why the subset cannot carry it, and names a Zod expression to
|
|
558
|
+
write instead. `Unsupported Zod type: ZodTuple` told a consumer the name of their own construct and
|
|
559
|
+
nothing they could act on.
|
|
560
|
+
|
|
561
|
+
- 93d061d: CORE-043: structured output now knows which transport can carry the schema before the first call
|
|
562
|
+
|
|
563
|
+
`run(input, { output })` asked every provider for `responseFormat: { type: 'json_schema' }`. A
|
|
564
|
+
provider whose surface cannot express that accepted the option and dropped it — and the schema was
|
|
565
|
+
stated in words only by the RETRY feedback turn, which runs on attempt two. So against such a
|
|
566
|
+
provider, attempt one carried nothing describing the required shape and could only succeed by luck:
|
|
567
|
+
the advertised three attempts were really two, and the first was spent discovering something the
|
|
568
|
+
capability table already knew.
|
|
569
|
+
|
|
570
|
+
A `(provider, model)` pair now resolves to a mechanism (`response_schema` / `json_object` / `none`)
|
|
571
|
+
and a provenance (`catalog` / `vendor-default` / `undeclared` / `unverified-endpoint`), and the
|
|
572
|
+
request is shaped to match at the one seam that holds both the resolved provider and the outgoing
|
|
573
|
+
messages. When the wire cannot carry the shape, the schema is stated in the prompt on the FIRST
|
|
574
|
+
attempt. Each structured request emits a `structured_output_transport` event reporting what the
|
|
575
|
+
request actually did.
|
|
576
|
+
|
|
577
|
+
- `IAIProvider.endpointIsVendorDefault?()` — a provider configured with a custom `baseURL` reports
|
|
578
|
+
it, so the runtime stops claiming enforcement a gateway may not provide. Separate from
|
|
579
|
+
`capabilityTable?()` on purpose: `@robota-sdk/agent-provider-openai` declares no table (nobody has
|
|
580
|
+
verified one) and must still be able to answer.
|
|
581
|
+
- DeepSeek's capability table declared `json_schema`; DeepSeek guarantees the response PARSES but
|
|
582
|
+
takes no schema parameter. Corrected to `json_object`.
|
|
583
|
+
- A provider that declares nothing is still sent the request unchanged — silence is not a denial.
|
|
584
|
+
|
|
585
|
+
- 39554a1: CORE-047: the model-configuration API is reachable before the first turn
|
|
586
|
+
|
|
587
|
+
`getModel()`, `setModel()` and `swapDefaultProvider()` refused on a freshly constructed agent with
|
|
588
|
+
`Agent must be fully initialized before ...`. So you had to ask the model a question before you could
|
|
589
|
+
ask which model you were using.
|
|
590
|
+
|
|
591
|
+
The state they guarded — the provider registry and the current `(provider, model)` pair — turned out
|
|
592
|
+
to be synchronous and derived entirely from config the constructor had already validated. It merely
|
|
593
|
+
lived inside the async initializer, next to work that genuinely is async (modules, plugins, the
|
|
594
|
+
execution service). Both steps now run in the constructor, so an agent knows which model it is
|
|
595
|
+
configured for from the moment it exists, and the readiness guard on those three methods protected
|
|
596
|
+
nothing and is gone.
|
|
597
|
+
|
|
598
|
+
A destroyed agent still refuses — and now says so accurately (`AIProviders was disposed`) instead of
|
|
599
|
+
misreporting teardown as missing initialization.
|
|
600
|
+
|
|
601
|
+
`Robota.ensureReady()` is unchanged and remains the way to complete the asynchronous half without
|
|
602
|
+
running a turn. It is no longer a precondition for reading or changing the model.
|
|
603
|
+
|
|
604
|
+
- d28430a: CORE-048: `resolveStructuredOutputCapability` is exported
|
|
605
|
+
|
|
606
|
+
`TStructuredOutputMechanism` and `TStructuredOutputProvenance` were already public while the function
|
|
607
|
+
that produces them was not — a caller could name the answer but not obtain it. Exporting it also lets
|
|
608
|
+
a consumer ask, before spending a call, what will happen to their schema against a given
|
|
609
|
+
`(provider, model)` pair.
|
|
610
|
+
|
|
611
|
+
No behaviour changed. This falls out of CORE-048, which asked whether a forced tool call should join
|
|
612
|
+
the mechanism vocabulary and answered no: the transport would need a provider that both lacks a
|
|
613
|
+
schema parameter and has enforceable strict tool arguments, and across this workspace that
|
|
614
|
+
intersection is empty.
|
|
615
|
+
|
|
616
|
+
- 07b627f: A local peer can now be answered, and what its turn may do depends on where it runs.
|
|
617
|
+
|
|
618
|
+
- `agent-core`: the permission evaluator takes a peer turn's authority as one more input
|
|
619
|
+
(`IPermissionEvaluationContext.peerTurn`), decided after the deny list and ceiling and before
|
|
620
|
+
bypass and allow rules. A peer on another host uses no tool. A peer on the same host may use an
|
|
621
|
+
inspect-class tool that declares `workspacePaths`, only when every named location resolves inside
|
|
622
|
+
the workspace and is not a credential (`isSecretPath`); write and execute tools are refused unless
|
|
623
|
+
enabled, and then every use asks. A tool declaring `repliesToPeer` exists only in a peer turn and
|
|
624
|
+
asks once the turn used another tool. New: `isToolAvailableInPeerTurn`, `TPeerReach`,
|
|
625
|
+
`IPeerTurnAuthority`. `IRunOptions.withholdHostedTools` leaves a provider's hosted tools out of a
|
|
626
|
+
run's requests (`nativeWebTools` with `false` withholds a hosted tool for one call).
|
|
627
|
+
- `agent-tools`: `Read` and `Glob` declare the arguments that say where they look. `Grep` does not:
|
|
628
|
+
it reads files it was never named, so a peer turn does not get it.
|
|
629
|
+
- `agent-session`: `ISessionRunOptions.peerReach` makes a run a peer turn for the permission policy;
|
|
630
|
+
every ask in it needs a fresh approval, and its requests carry no provider-hosted tool. `ISessionOptions.allowPeerChanges` enables write and execute
|
|
631
|
+
tools for same-host peer turns.
|
|
632
|
+
- `agent-interface-session`: `ISubmitOptions.peer` (`IPeerTurnContext`) carries a peer turn's reach,
|
|
633
|
+
the message it answers and the session a reply goes to.
|
|
634
|
+
- `agent-interface-session-mobility`: `IPeerMessage.inReplyTo` threads a conversation;
|
|
635
|
+
`peerReachOf(admission)` maps admission to a reach.
|
|
636
|
+
- `agent-framework`: a peer turn is offered what its origin allows, and a new `peer_reply` tool
|
|
637
|
+
answers the peer that sent the message, threaded to it. The setting `peers.allowChanges` enables
|
|
638
|
+
write and execute tools for same-host peer turns.
|
|
639
|
+
- `agent-ui-terminal`: a permission prompt in a peer turn names the requesting peer.
|
|
640
|
+
- `agent-cli`: incoming peer turns carry their reach and reply route; a conversation is limited in
|
|
641
|
+
depth and in how often this session answers it, and a reply over a limit is not sent and the
|
|
642
|
+
operator is told.
|
|
643
|
+
- `agent-provider-anthropic`, `agent-provider-openai-compatible` (Qwen): a request whose
|
|
644
|
+
`nativeWebTools` sets a hosted tool to `false` is sent without it.
|
|
645
|
+
|
|
646
|
+
- d0de5b2: A peer session's message now reaches the model as a peer's, and a peer turn runs on the external
|
|
647
|
+
baseline.
|
|
648
|
+
|
|
649
|
+
- `agent-core` marks every user message whose driver id starts with `peer:` as
|
|
650
|
+
`<peer_message from="…">…</peer_message>` in the outgoing request — both the round and the forced
|
|
651
|
+
summary — while the stored history keeps the text as sent. Wrapper-shaped text in user and tool
|
|
652
|
+
messages is escaped, and an id that is not a plain identifier is printed as `peer:unverified`.
|
|
653
|
+
New exports: `peerDriverOf`, `printablePeerDriver`.
|
|
654
|
+
- `agent-session`'s conversation transcript (compaction, advisor) labels a peer message with the same
|
|
655
|
+
printable id, so no rendering echoes a sender-chosen id that is not a plain identifier.
|
|
656
|
+
- `agent-framework` runs a `peer` turn like an `external` one — no tools (`toolChoice: 'none'`), no
|
|
657
|
+
`@path` expansion, no context references — and adds a per-turn system statement that the message
|
|
658
|
+
came from another session and carries no authority. A `peer` turn must carry a `peer:` driver id.
|
|
659
|
+
|
|
660
|
+
- d6b9404: Remove polynomial-ReDoS backtracking (SEC-003, CodeQL `js/polynomial-redos`) from three parsers whose input is not repo-controlled.
|
|
661
|
+
|
|
662
|
+
`parseStructuredResponseText` (agent-core) parses raw model output; its fenced-code-block regex used `\s*\n`, and because `\s` also matches a newline the two overlapped, so an unterminated fence containing many blank lines was rejected in O(n^2) — 12.7s for a 400 KB string, now ~1ms. The whitespace run is now restricted to horizontal whitespace, which makes the newline split point unique.
|
|
663
|
+
|
|
664
|
+
`/schedule cron` and `/monitor` (agent-command) are declared `modelInvocable: true`, so their argument string is composed by the model. Both matched the trailing instruction with `\s+(.+)$`; since `.` also matches a space the split point was ambiguous and a non-matching argument cost O(n^2) — ~15s for a 200 KB argument, now <1ms. The instruction is now required to start with a non-space character, which pins the split point without changing which inputs are accepted.
|
|
665
|
+
|
|
666
|
+
No behaviour change: the set of accepted inputs and the parsed values are identical in every case.
|
|
667
|
+
|
|
668
|
+
- 9814afc: Type-SSOT convergence (TYPE-003; re-audit CONTRACT-002/003/011/012 + RUNTIME-47 + STRUCT-04). Behavior is unchanged — this is a type-level refactor. `ITokenUsage` (agent-core) is confirmed as the usage-triple SSOT: `ISessionUsageTotals` and `IBackgroundTaskUsage` become aliases, and every inline `{ promptTokens; completionTokens; totalTokens }` copy (service/orchestration/executor/remote-client shapes) now references the SSOT (structurally identical → patch). The subagent-job contracts derive from the background-task SSOT — `TSubagentJobStatus = Exclude<TBackgroundTaskStatus, 'paused'>`, mode alias, and a `Pick`-projection `ISubagentJobState` — with a compile-enforced parity test so a drifting hand copy can no longer exist. `@robota-sdk/agent-session` is minor because the public `ISessionRecord` type is now the typed `IInteractiveSessionRecord` alias (previously a relaxed `unknown[]` mirror): runtime behavior of `SessionStore` is identical, but downstream code that assigned loose payloads to the record's fields may need explicit casts at its own trust boundary (the framework store facade's `as unknown as` cast bridge is deleted). agent-session's duplicate `@robota-sdk/agent-core` deps/devDeps declaration is also removed (STRUCT-04).
|
|
669
|
+
|
|
3
670
|
## 3.0.0-beta.79
|
|
4
671
|
|
|
5
672
|
## 3.0.0-beta.78
|