mistri 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +469 -4
- data/CONTRIBUTING.md +52 -0
- data/README.md +289 -385
- data/SECURITY.md +40 -0
- data/UPGRADING.md +640 -0
- data/assets/logo-animated.svg +30 -0
- data/assets/logo-dark.svg +14 -0
- data/assets/logo-light.svg +14 -0
- data/assets/logo.svg +14 -0
- data/assets/social-preview.png +0 -0
- data/docs/README.md +87 -0
- data/docs/context-and-workspaces.md +378 -0
- data/docs/mcp.md +366 -0
- data/docs/reliability.md +450 -0
- data/docs/sessions.md +295 -0
- data/docs/sub-agents.md +401 -0
- data/docs/tool-contracts.md +324 -0
- data/examples/approval.rb +36 -0
- data/examples/browser.rb +27 -0
- data/examples/page_editor.rb +31 -0
- data/examples/quickstart.rb +21 -0
- data/lib/generators/mistri/install/install_generator.rb +7 -3
- data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
- data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
- data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
- data/lib/mistri/agent.rb +575 -55
- data/lib/mistri/budget.rb +26 -1
- data/lib/mistri/child.rb +72 -16
- data/lib/mistri/compaction.rb +26 -10
- data/lib/mistri/compactor.rb +34 -11
- data/lib/mistri/console.rb +28 -7
- data/lib/mistri/content.rb +9 -3
- data/lib/mistri/dispatchers.rb +14 -12
- data/lib/mistri/errors.rb +83 -4
- data/lib/mistri/event.rb +24 -8
- data/lib/mistri/event_delivery.rb +60 -0
- data/lib/mistri/locks.rb +3 -3
- data/lib/mistri/mcp/client.rb +74 -19
- data/lib/mistri/mcp/egress.rb +216 -0
- data/lib/mistri/mcp/oauth.rb +476 -127
- data/lib/mistri/mcp/wires.rb +115 -23
- data/lib/mistri/mcp.rb +42 -8
- data/lib/mistri/message.rb +21 -11
- data/lib/mistri/models.rb +160 -22
- data/lib/mistri/providers/anthropic/assembler.rb +282 -44
- data/lib/mistri/providers/anthropic/serializer.rb +14 -9
- data/lib/mistri/providers/anthropic.rb +29 -6
- data/lib/mistri/providers/fake.rb +26 -10
- data/lib/mistri/providers/gemini/assembler.rb +148 -21
- data/lib/mistri/providers/gemini/serializer.rb +78 -9
- data/lib/mistri/providers/gemini.rb +31 -5
- data/lib/mistri/providers/openai/assembler.rb +337 -60
- data/lib/mistri/providers/openai/serializer.rb +13 -12
- data/lib/mistri/providers/openai.rb +29 -5
- data/lib/mistri/providers/schema_capabilities.rb +214 -0
- data/lib/mistri/result.rb +1 -1
- data/lib/mistri/schema.rb +893 -75
- data/lib/mistri/session.rb +560 -48
- data/lib/mistri/sinks/coalesced.rb +17 -10
- data/lib/mistri/spawner.rb +111 -61
- data/lib/mistri/sse.rb +57 -14
- data/lib/mistri/stores/active_record.rb +1 -1
- data/lib/mistri/stores/memory.rb +21 -2
- data/lib/mistri/sub_agent/execution.rb +81 -0
- data/lib/mistri/sub_agent/runtime.rb +297 -0
- data/lib/mistri/sub_agent.rb +124 -87
- data/lib/mistri/task_output.rb +24 -6
- data/lib/mistri/tool.rb +93 -13
- data/lib/mistri/tool_arguments.rb +377 -0
- data/lib/mistri/tool_call.rb +43 -9
- data/lib/mistri/tool_context.rb +4 -2
- data/lib/mistri/tool_executor.rb +117 -26
- data/lib/mistri/tool_result.rb +15 -10
- data/lib/mistri/tools/edit_file.rb +62 -8
- data/lib/mistri/tools.rb +41 -4
- data/lib/mistri/transport.rb +149 -44
- data/lib/mistri/usage.rb +65 -13
- data/lib/mistri/version.rb +1 -1
- data/lib/mistri/workspace/active_record.rb +183 -3
- data/lib/mistri/workspace/directory.rb +28 -8
- data/lib/mistri/workspace/memory.rb +34 -9
- data/lib/mistri/workspace/single.rb +62 -5
- data/lib/mistri/workspace.rb +39 -0
- data/lib/mistri.rb +6 -1
- data/mistri.gemspec +34 -0
- metadata +31 -3
data/SECURITY.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
Report vulnerabilities privately through GitHub security advisories:
|
|
4
|
+
https://github.com/mcheemaa/mistri/security/advisories/new
|
|
5
|
+
|
|
6
|
+
Please do not open public issues for security reports. You will get a
|
|
7
|
+
response within a few days, and credit in the fix's release notes unless
|
|
8
|
+
you prefer otherwise.
|
|
9
|
+
|
|
10
|
+
Notes worth knowing when assessing reports:
|
|
11
|
+
|
|
12
|
+
- mistri has zero runtime dependencies; supply-chain surface is Ruby's
|
|
13
|
+
standard library.
|
|
14
|
+
- Tokens and API keys are never logged by the gem and never persisted by
|
|
15
|
+
the gem itself; storage is the host application's, and the generated
|
|
16
|
+
Rails models encrypt token columns.
|
|
17
|
+
- Remote MCP and OAuth URLs default to public HTTPS. Mistri rejects credentials,
|
|
18
|
+
fragments, private, loopback, link-local, shared, multicast, documentation,
|
|
19
|
+
and reserved destinations; validates every DNS answer; and pins an approved
|
|
20
|
+
address while preserving hostname-based TLS verification. Every connection
|
|
21
|
+
cycle validates a fresh DNS set, including keep-alive reconnect cycles, and
|
|
22
|
+
never falls back from an unsafe set to an old address. Approved candidates
|
|
23
|
+
from one set are tried only before request transmission. MCP and server-side
|
|
24
|
+
OAuth requests do not follow redirects or inherit ambient HTTP proxy settings.
|
|
25
|
+
The user's browser resolves the authorization endpoint independently.
|
|
26
|
+
- A host can explicitly approve an internal HTTPS destination with
|
|
27
|
+
`allow_non_public:`. Plain HTTP remains limited to an explicitly approved,
|
|
28
|
+
actually resolved loopback address. The callback is part of the host's
|
|
29
|
+
security boundary and should match both an expected hostname and IP range.
|
|
30
|
+
- OAuth protected-resource and issuer metadata are bound to exact identifiers.
|
|
31
|
+
Pre-registered clients require the issuer trusted at registration, and token
|
|
32
|
+
endpoints are rediscovered from that issuer before credentials are sent.
|
|
33
|
+
OAuth response bodies are read incrementally with a 256 KiB limit and
|
|
34
|
+
compression disabled; server-controlled descriptions are not copied into
|
|
35
|
+
token errors.
|
|
36
|
+
- Provider and MCP JSON bodies, individual SSE lines, and stdio records default
|
|
37
|
+
to an 8 MiB `max_record_bytes:` ceiling. The total size of an SSE stream is
|
|
38
|
+
not capped when each record remains within that boundary. Error responses
|
|
39
|
+
retain at most a 500-byte valid UTF-8 preview. HTTP status, header, chunk-size,
|
|
40
|
+
and trailer lines are parsed by Net::HTTP before this body boundary applies.
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# Upgrading Mistri
|
|
2
|
+
|
|
3
|
+
Mistri follows SemVer from 0.x: patches fix behavior, while a minor release may
|
|
4
|
+
add features and intentional contract changes. Every change belongs in the
|
|
5
|
+
[changelog](CHANGELOG.md); this guide extracts only the work an existing host
|
|
6
|
+
may need to perform.
|
|
7
|
+
|
|
8
|
+
Upgrade a representative application and persisted session set in staging
|
|
9
|
+
before production. Do not treat a passing bundle update as a session migration
|
|
10
|
+
test.
|
|
11
|
+
|
|
12
|
+
## From 0.5.x to 0.6.x
|
|
13
|
+
|
|
14
|
+
This is a safety-focused minor upgrade. The built-in Anthropic, OpenAI, and
|
|
15
|
+
Gemini providers preserve ordinary 0.5 sessions, including the repeated
|
|
16
|
+
`call_N` IDs older Gemini and Fake-provider turns generated after the earlier
|
|
17
|
+
occurrence settled. There is no blanket session rewrite step.
|
|
18
|
+
|
|
19
|
+
Hosts that used implicit tool schemas, mutated arguments, supplied custom
|
|
20
|
+
providers or Session stores, dispatched background children, used document
|
|
21
|
+
workspaces, generated MCP OAuth models, stored sessions in MySQL, or configured
|
|
22
|
+
cost budgets need to review the sections below.
|
|
23
|
+
|
|
24
|
+
### Checklist
|
|
25
|
+
|
|
26
|
+
- [ ] Declare every argument a model may send to a formerly schema-less tool.
|
|
27
|
+
- [ ] Remove handler or policy mutations of model argument hashes and arrays.
|
|
28
|
+
- [ ] Replace implicit coercion with an explicit per-tool normalizer where it is
|
|
29
|
+
genuinely part of the contract.
|
|
30
|
+
- [ ] Make event subscribers tolerate new event types and read `tool_error`
|
|
31
|
+
structurally.
|
|
32
|
+
- [ ] Audit synchronous event subscribers: their exact exceptions now escape
|
|
33
|
+
`run` or `resume` and may follow durable facts or external side effects.
|
|
34
|
+
- [ ] Ensure at most one `run` or `resume` is active per Session across all
|
|
35
|
+
processes.
|
|
36
|
+
- [ ] Treat each approval as single-assignment; resolve quorum or multi-reviewer
|
|
37
|
+
policy in the host before calling `approve` or `deny`.
|
|
38
|
+
- [ ] Verify every successful custom Session-store append is visible to later
|
|
39
|
+
loads in the same stable per-session order.
|
|
40
|
+
- [ ] Audit custom provider tool-call IDs, names, arguments, and metadata.
|
|
41
|
+
- [ ] Widen Active Record session payloads to LONGTEXT on MySQL or Trilogy.
|
|
42
|
+
- [ ] Update generated MCP OAuth models to persist issuer and share egress
|
|
43
|
+
policy across the complete flow.
|
|
44
|
+
- [ ] Review the new MCP and provider response limits and ambiguous-delivery
|
|
45
|
+
behavior.
|
|
46
|
+
- [ ] Configure a deterministic standard service tier for any cost budget.
|
|
47
|
+
- [ ] Remove intentional symlinks from model-controlled Directory workspaces.
|
|
48
|
+
- [ ] Add a runtime factory to every background spawner and queue worker.
|
|
49
|
+
- [ ] Review queued child specs and ensure their exact `tool_names` grant is
|
|
50
|
+
still intended before executing them under 0.6.
|
|
51
|
+
- [ ] Keep qualifying Active Record workspaces outside host transactions and
|
|
52
|
+
assert `atomic_writes?` at boot when lost-update protection is required.
|
|
53
|
+
- [ ] Replace mutation of values returned by `Workspace::Memory#read` with an
|
|
54
|
+
explicit `write`.
|
|
55
|
+
- [ ] Use anchored `edit_file`, not blind `write_file`, when concurrent changes
|
|
56
|
+
must be preserved.
|
|
57
|
+
- [ ] Exercise active persisted sessions, approvals, tool failures, compaction,
|
|
58
|
+
and one real call per configured provider in staging.
|
|
59
|
+
|
|
60
|
+
## Background children reconstruct their runtime
|
|
61
|
+
|
|
62
|
+
A dispatcher no longer captures the spawn-time provider, Tool objects, Agent
|
|
63
|
+
options, or their workspace closures. Every spawner with `dispatcher:` must now
|
|
64
|
+
provide a host-owned `runtime_factory:`. Mistri invokes it inside the worker and
|
|
65
|
+
requires a `Mistri::SubAgent::Runtime`:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
runtime_factory = lambda do |spec|
|
|
69
|
+
workspace = HostWorkspaces.for_child(spec.fetch("session_id"))
|
|
70
|
+
provider = HostModels.build(spec.fetch("model"))
|
|
71
|
+
tools = HostTools.build(spec.fetch("tool_names"), workspace: workspace)
|
|
72
|
+
|
|
73
|
+
Mistri::SubAgent::Runtime.new(
|
|
74
|
+
provider: provider,
|
|
75
|
+
system: HostAgents.system_for(spec),
|
|
76
|
+
tools: tools,
|
|
77
|
+
cleanup: -> { HostAgents.release(provider: provider, tools: tools) },
|
|
78
|
+
budget: HostAgents.child_budget,
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
spawn = Mistri::SubAgent.spawner(
|
|
83
|
+
provider: child_provider,
|
|
84
|
+
tools: declared_tools,
|
|
85
|
+
dispatcher: Mistri::Dispatchers::Thread.new,
|
|
86
|
+
runtime_factory: runtime_factory,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The provider model and unique Tool names returned by the factory must match the
|
|
91
|
+
versioned dispatch spec exactly. Mistri rejects missing, additional, duplicate,
|
|
92
|
+
renamed, or reconstructed statically approval-gated tools before the provider
|
|
93
|
+
runs. This turns `tool_names` into a durable capability grant rather than queue
|
|
94
|
+
metadata. A versioned spec and its exact parent routing are also stored in the
|
|
95
|
+
child Session; a changed queue copy raises without failing or reporting the
|
|
96
|
+
legitimate child.
|
|
97
|
+
|
|
98
|
+
`cleanup:` is optional and called exactly once after Mistri accepts a Runtime,
|
|
99
|
+
including after validation or execution failure. Use it for per-child provider
|
|
100
|
+
sockets, MCP subprocesses, and other host-owned resources. It never masks the
|
|
101
|
+
primary error. `skills:` is rejected because it would add an
|
|
102
|
+
undeclared `read_skill` tool. Spawner Agent options apply only to inline
|
|
103
|
+
children; declare background Agent options on the Runtime.
|
|
104
|
+
|
|
105
|
+
Sub-agent Agent options are now limited to `budget`, `max_concurrency`,
|
|
106
|
+
`transform_context`, `compaction`, `retries`, `skills`, `before_tool`,
|
|
107
|
+
`after_tool`, and application `context`; background Runtime still rejects
|
|
108
|
+
`skills`. Lifecycle keys such as `session`, `task`, `signal`, and `emit` are
|
|
109
|
+
owned by the child and cannot be overridden through generic keyword options.
|
|
110
|
+
|
|
111
|
+
The model-facing `workspace` argument has been removed. It never cloned a Tool
|
|
112
|
+
closure or proved isolation. Existing queued specs that contain `workspace`
|
|
113
|
+
remain readable, but new specs omit it. Build child-scoped resources from
|
|
114
|
+
trusted host state and stable identifiers such as `session_id`; a retry should
|
|
115
|
+
reopen the same durable scope. Inline children still use the exact objects the
|
|
116
|
+
host supplied and may share their backends.
|
|
117
|
+
|
|
118
|
+
Queue jobs should pass the factory into the lifecycle entry point:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
Mistri::SubAgent.run_dispatched(
|
|
122
|
+
spec,
|
|
123
|
+
store: mistri_store,
|
|
124
|
+
runtime_factory: HostAgents.method(:runtime_for),
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Factory exceptions are retryable by default: the child remains non-terminal
|
|
129
|
+
and the exception returns to the queue. Pass `retry_factory_errors: false` on a
|
|
130
|
+
final attempt to persist and report the failure before the exception re-raises.
|
|
131
|
+
The flag does not control the job system's retry or discard decision. The
|
|
132
|
+
built-in Thread and Inline runners use terminal behavior because they have no
|
|
133
|
+
durable retry owner.
|
|
134
|
+
|
|
135
|
+
If the queue copy differs from the stored grant, `run_dispatched` raises
|
|
136
|
+
`Mistri::DispatchGrantError` without touching the legitimate child. Treat the
|
|
137
|
+
unchanged job as poison: discard it and alert rather than retrying it. This
|
|
138
|
+
error is reserved for Mistri's stored-grant binding; do not raise it from a
|
|
139
|
+
runtime factory.
|
|
140
|
+
|
|
141
|
+
The compatible direct `provider:`, `system:`, and `tools:` form remains, but it
|
|
142
|
+
now receives the same exact model and tool-grant validation. A job that used to
|
|
143
|
+
pass its entire registry must instead reconstruct only the Tool objects named
|
|
144
|
+
by `spec.fetch("tool_names")`.
|
|
145
|
+
Legacy 0.5 specs keep their serialized grant, including any broad grant created
|
|
146
|
+
by the old empty-Definition bug. Their old child entries also have no stored
|
|
147
|
+
copy against which Mistri can verify capability or report-routing changes.
|
|
148
|
+
Drain or review unversioned queued work before relying on the new boundary.
|
|
149
|
+
|
|
150
|
+
Tool selection now distinguishes omission from an explicit empty list. Omitted
|
|
151
|
+
`tools` means the general pool or a typed Definition's defaults. `tools: []`
|
|
152
|
+
means no tools for either worker shape, and a typed Definition that declares no
|
|
153
|
+
tools no longer inherits the general pool.
|
|
154
|
+
|
|
155
|
+
## Tool schemas are enforced locally
|
|
156
|
+
|
|
157
|
+
Model-originated arguments now cross ownership and schema validation before
|
|
158
|
+
`before_tool`, approval predicates, `ends_turn`, MCP, or handlers.
|
|
159
|
+
|
|
160
|
+
Invalid input receives a bounded error result the model can correct. Valid
|
|
161
|
+
sibling calls still run. Mistri does not coerce values.
|
|
162
|
+
|
|
163
|
+
### Schema-less tools are closed
|
|
164
|
+
|
|
165
|
+
In 0.5, the no-argument wire schema did not explicitly reject extra fields. In
|
|
166
|
+
0.6, a tool without `schema:` or `input_schema:` is a closed no-argument tool.
|
|
167
|
+
|
|
168
|
+
No change is needed when the tool truly accepts no arguments:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Mistri::Tool.define("current_time", "Returns the current time.") do
|
|
172
|
+
Time.now.utc.iso8601
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Declare every accepted field when the handler uses arguments:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
Mistri::Tool.define("lookup", "Looks up one account.", schema: lambda {
|
|
180
|
+
string :account_id, "Account ID", required: true
|
|
181
|
+
}) do |args|
|
|
182
|
+
Accounts.fetch(args.fetch("account_id"))
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
For an intentionally freeform object, say so explicitly:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
input_schema = {
|
|
190
|
+
type: "object",
|
|
191
|
+
additionalProperties: true,
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The schema-less wire shape changes once, so provider prompt caches for affected
|
|
196
|
+
tool sets will re-warm after deployment.
|
|
197
|
+
|
|
198
|
+
### Supplied objects remain open by default
|
|
199
|
+
|
|
200
|
+
JSON Schema object properties do not close the object. If extra keys must fail,
|
|
201
|
+
use a raw schema with `additionalProperties: false`. Otherwise extract declared
|
|
202
|
+
fields instead of mass-assigning the complete argument hash.
|
|
203
|
+
|
|
204
|
+
### JSON Schema dialect and subset
|
|
205
|
+
|
|
206
|
+
Tool schemas use JSON Schema 2020-12. Replace legacy tuple-array `items: [...]`
|
|
207
|
+
with `prefixItems`. A supplied explicit older dialect is rejected.
|
|
208
|
+
|
|
209
|
+
Core enforces its documented portable subset. Use `argument_validator:` for
|
|
210
|
+
domain additions and `complete_argument_validator:` only when the host
|
|
211
|
+
validator implements the entire schema. Argument-applicable non-empty
|
|
212
|
+
`patternProperties` requires complete authority. External references are never
|
|
213
|
+
resolved.
|
|
214
|
+
|
|
215
|
+
Task mode now rejects an assertion Mistri cannot guarantee locally instead of
|
|
216
|
+
claiming validated output and leaving part of the schema to provider guidance.
|
|
217
|
+
Freeform objects cannot become strict task output without changing meaning.
|
|
218
|
+
|
|
219
|
+
See [Tool contracts](docs/tool-contracts.md).
|
|
220
|
+
|
|
221
|
+
### Schema objects are canonical and immutable
|
|
222
|
+
|
|
223
|
+
`Tool#input_schema` and `Schema.strict` now expose deeply frozen canonical JSON
|
|
224
|
+
with String keys. Replace Symbol-key access and mutation:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# Before
|
|
228
|
+
tool.input_schema[:properties][:account_id] = rule
|
|
229
|
+
|
|
230
|
+
# After
|
|
231
|
+
account_schema = tool.input_schema.fetch("properties").fetch("account_id")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Build a new schema when the host needs a changed contract. `Tool.define` now
|
|
235
|
+
rejects supplying both `schema:` and `input_schema:`; choose one. Calling
|
|
236
|
+
`Schema.build` without a block now raises because it declares no argument
|
|
237
|
+
intent.
|
|
238
|
+
|
|
239
|
+
## Arguments are immutable and policy sees validated types
|
|
240
|
+
|
|
241
|
+
Completed arguments and the canonical schema are deeply frozen. Code that
|
|
242
|
+
mutated model input must copy the portion it owns:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
# Before: mutates the value shared with policy and replay.
|
|
246
|
+
args["tags"] << "processed"
|
|
247
|
+
|
|
248
|
+
# After: build application-owned data.
|
|
249
|
+
tags = args.fetch("tags", []).dup
|
|
250
|
+
tags << "processed"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
For a fully mutable JSON copy:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
owned = JSON.parse(JSON.generate(args))
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Approval predicates can now rely on declared types. Remove defensive coercion
|
|
260
|
+
that changes authorization meaning:
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# Do not turn "all" into zero at an authorization boundary.
|
|
264
|
+
approval_policy = ->(args) { args.fetch("total_usd") > 500 }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
When a host intentionally accepts a legacy alias or representation, use
|
|
268
|
+
`argument_normalizer:`. Its output is what validation, policy, approval, and
|
|
269
|
+
the handler see. It runs once before validation and does not run again on
|
|
270
|
+
resume.
|
|
271
|
+
|
|
272
|
+
Direct `Tool#call` remains a trusted host path. It applies the tool's normalizer
|
|
273
|
+
but does not invoke the Agent's model-input validation boundary.
|
|
274
|
+
|
|
275
|
+
## Tool lifecycle and event subscribers
|
|
276
|
+
|
|
277
|
+
Tool results now carry a structured error fact. A handler may return
|
|
278
|
+
`Mistri::ToolResult.new(content: ..., error: true)`, and Mistri sets the same
|
|
279
|
+
fact for invalid, blocked, unknown, interrupted, timed-out, or failed calls.
|
|
280
|
+
|
|
281
|
+
`:tool_started` is emitted only when a resolved call commits to handler
|
|
282
|
+
invocation. `:tool_result` always carries a boolean `event.tool_error`; new
|
|
283
|
+
persisted tool messages carry the same value. Legacy messages without the field
|
|
284
|
+
remain unknown rather than being classified from prose.
|
|
285
|
+
|
|
286
|
+
`:tool_started` and handler progress callbacks originate on tool worker threads.
|
|
287
|
+
Custom sinks must be thread-safe. Code that manually constructs a
|
|
288
|
+
`:tool_result` `Mistri::Event` must now supply a boolean `tool_error` matching
|
|
289
|
+
the Message.
|
|
290
|
+
|
|
291
|
+
An exception from a synchronous subscriber now propagates unchanged from
|
|
292
|
+
`run` or `resume`. Mistri does not retry it or reinterpret it as a provider,
|
|
293
|
+
transport, hook, tool, parse, or compaction failure. Delivery is not a
|
|
294
|
+
transaction: some events follow durable Session appends, and handler progress
|
|
295
|
+
can follow an external side effect. Catch subscriber failures at the host
|
|
296
|
+
boundary, reconcile the Session and the tool's domain idempotency state, and
|
|
297
|
+
retry only under host policy. See [Reliability](docs/reliability.md#events-and-sinks).
|
|
298
|
+
|
|
299
|
+
Update exhaustive event switches to ignore unknown future members:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
case event.type
|
|
303
|
+
when :text_delta
|
|
304
|
+
stream(event.delta)
|
|
305
|
+
when :tool_started
|
|
306
|
+
mark_running(event.tool_call)
|
|
307
|
+
when :tool_result
|
|
308
|
+
settle(event.tool_call, failed: event.tool_error)
|
|
309
|
+
else
|
|
310
|
+
# Event types are an extensible union.
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Do not decide failure by matching prefixes such as `"Error:"`. Human denial is
|
|
315
|
+
an expected control result and is not marked as execution failure.
|
|
316
|
+
|
|
317
|
+
Missing documents in `read_file`, `find_in_file`, and `edit_file`, plus an
|
|
318
|
+
`edit_file` replacement that cannot be applied, now carry `tool_error: true`.
|
|
319
|
+
Their actionable model-facing text is unchanged.
|
|
320
|
+
|
|
321
|
+
The error fact never authorizes mechanical replay. A handler can commit a side
|
|
322
|
+
effect before it raises.
|
|
323
|
+
|
|
324
|
+
Public value records gained fields: `ToolCall` adds `arguments_error` and
|
|
325
|
+
`provider_call_id`, `ToolResult` adds `error`, and Message/Event records add
|
|
326
|
+
`tool_error`. Keyword construction remains compatible through defaults, but
|
|
327
|
+
exact positional deconstruction or pattern matching can break. Prefer named
|
|
328
|
+
accessors and key patterns, and treat value and event records as extensible.
|
|
329
|
+
|
|
330
|
+
## Custom provider contract
|
|
331
|
+
|
|
332
|
+
Built-in providers already satisfy the new envelope. A custom provider must
|
|
333
|
+
return completed `Mistri::ToolCall` values with:
|
|
334
|
+
|
|
335
|
+
- a non-empty, valid UTF-8 String ID;
|
|
336
|
+
- an ID unique for new calls over the active Session;
|
|
337
|
+
- a non-empty, valid UTF-8 String name;
|
|
338
|
+
- a JSON object for valid arguments, or a durable `arguments_error` for
|
|
339
|
+
malformed encoded input;
|
|
340
|
+
- non-empty valid UTF-8 signature and provider call ID strings when present;
|
|
341
|
+
- provider correlation IDs unique within an assistant turn.
|
|
342
|
+
|
|
343
|
+
If any completed call makes the assistant turn unpairable, Mistri rejects the
|
|
344
|
+
whole provider attempt before persistence, policy, or sibling execution. The
|
|
345
|
+
ordinary provider retry asks for a clean turn with unchanged history.
|
|
346
|
+
|
|
347
|
+
`ToolCall#arguments_error?` is the stable predicate. Its non-nil String is an
|
|
348
|
+
opaque diagnostic, not a public enum.
|
|
349
|
+
|
|
350
|
+
Audit active custom-provider sessions without mutating them:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
active_session_ids.each do |session_id|
|
|
354
|
+
session = Mistri::Session.new(store: store, id: session_id)
|
|
355
|
+
session.tool_control_state # validates persisted call and approval identity
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The legacy repeated `call_N` exception is deliberately limited to settled 0.5
|
|
360
|
+
Gemini and Fake-provider history without provider correlation IDs. Do not copy
|
|
361
|
+
that shape into a custom provider.
|
|
362
|
+
|
|
363
|
+
Fake-provider call IDs are now namespaced instead of restarting at `call_1`,
|
|
364
|
+
`call_2`, and so on. Script an explicit `id:` when a test or persisted fixture
|
|
365
|
+
needs deterministic identity.
|
|
366
|
+
|
|
367
|
+
If an old custom log contains malformed IDs that cannot be paired safely, keep
|
|
368
|
+
the old application version available long enough to complete or retire that
|
|
369
|
+
session according to host policy. Do not invent replacement IDs in replay; they
|
|
370
|
+
would corrupt tool-result correlation.
|
|
371
|
+
|
|
372
|
+
## Approval decisions are single-assignment
|
|
373
|
+
|
|
374
|
+
The first valid approval decision in the Session store's durable order is
|
|
375
|
+
final. Repeating the same `approve` or `deny` call is idempotent and does not add
|
|
376
|
+
another entry once the winner is visible. The first note also wins. A later
|
|
377
|
+
conflicting call raises `Mistri::ConfigurationError`; it cannot revoke the
|
|
378
|
+
winner.
|
|
379
|
+
|
|
380
|
+
Two callers can still read the parked request before either appends. Both
|
|
381
|
+
attempts may therefore appear in the raw append-only log. Mistri re-reads the
|
|
382
|
+
ordered history after append so the winner returns normally and a conflicting
|
|
383
|
+
loser raises. Losing entries are validated but inert, including a stale loser
|
|
384
|
+
that arrives after the tool result. They no longer make the Session require a
|
|
385
|
+
repair step.
|
|
386
|
+
|
|
387
|
+
This is single-assignment, not quorum. If two people must agree, or a denial
|
|
388
|
+
must outrank an approval, implement that policy transactionally in the host and
|
|
389
|
+
call Session only after the host has one final decision. A custom Session store
|
|
390
|
+
must make a successful append visible to subsequent loads while preserving its
|
|
391
|
+
stable total order.
|
|
392
|
+
|
|
393
|
+
## Session stores
|
|
394
|
+
|
|
395
|
+
### MySQL and Trilogy payload width
|
|
396
|
+
|
|
397
|
+
The generated Active Record session table now uses LONGTEXT for `payload`. An
|
|
398
|
+
existing table is not changed by updating the generator:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
class WidenAgentEntryPayload < ActiveRecord::Migration[7.2]
|
|
402
|
+
def change
|
|
403
|
+
change_column :agent_entries, :payload, :text, size: :long, null: false
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Use the migration version appropriate for the host application. PostgreSQL
|
|
409
|
+
`text` needs no width change.
|
|
410
|
+
|
|
411
|
+
### Store value ownership
|
|
412
|
+
|
|
413
|
+
`Mistri::Stores::Memory` now owns data on append and returns fresh mutable
|
|
414
|
+
snapshots on load, matching stores that decode JSON on every read. A custom
|
|
415
|
+
store should not rely on Mistri mutating a previously returned entry object.
|
|
416
|
+
|
|
417
|
+
### Active Session execution
|
|
418
|
+
|
|
419
|
+
Mistri does not lease top-level Session execution. The host must ensure that at
|
|
420
|
+
most one `run` or `resume` is active for a Session across all processes.
|
|
421
|
+
Concurrent-safe store appends and child lock adapters do not provide this
|
|
422
|
+
guarantee. Keep external writes idempotent or reconcilable across the
|
|
423
|
+
handler-commit/result-append gap.
|
|
424
|
+
|
|
425
|
+
`Session#entries` and serialized usage are extensible records. Raw host readers
|
|
426
|
+
must ignore unknown entry types and keys. This release adds lifecycle, approval
|
|
427
|
+
provenance, unpriced-attempt, and `cost.known` data.
|
|
428
|
+
|
|
429
|
+
## Workspace editing and conditional writes
|
|
430
|
+
|
|
431
|
+
`edit_file` now uses conditional writes when a workspace explicitly advertises
|
|
432
|
+
`atomic_writes?`. Existing custom workspaces that implement only `read`,
|
|
433
|
+
`write`, `delete`, and `list` keep their 0.5 behavior. A custom workspace must
|
|
434
|
+
claim atomic support only when its `snapshot` and `compare_and_write` methods
|
|
435
|
+
satisfy the complete contract in
|
|
436
|
+
[Context and workspaces](docs/context-and-workspaces.md#workspaces-and-document-tools).
|
|
437
|
+
|
|
438
|
+
Only anchored `edit_file` uses this protocol. `write_file` remains an
|
|
439
|
+
intentional blind whole-document replacement, and deletion has no conditional
|
|
440
|
+
form. Use an anchored edit or a host tool with a stronger domain contract when
|
|
441
|
+
concurrent writers must preserve unrelated changes.
|
|
442
|
+
|
|
443
|
+
A qualifying `Mistri::Workspace::ActiveRecord` now takes the conditional path
|
|
444
|
+
on PostgreSQL, or on an InnoDB table through MySQL2 or Trilogy. It verifies the
|
|
445
|
+
primary key, non-null path, content, and scope columns, and an exact unique
|
|
446
|
+
scope/path index before advertising the capability. Atomic snapshots and edits
|
|
447
|
+
reject when the connection already has an open host transaction, because the
|
|
448
|
+
adapter cannot safely join that transaction's isolation and lock lifetime. Run
|
|
449
|
+
the Agent outside the host transaction. Unsupported storage retains the legacy
|
|
450
|
+
path; assert `workspace.atomic_writes?` at boot rather than accepting that
|
|
451
|
+
fallback when lost-update protection is required.
|
|
452
|
+
|
|
453
|
+
`Mistri::Workspace::Memory#read` now returns an owned copy. Call `write` to
|
|
454
|
+
persist a changed value; mutating a previously returned String no longer changes
|
|
455
|
+
workspace state. Its conditional writes protect only threads sharing that
|
|
456
|
+
Memory instance, not separate processes.
|
|
457
|
+
|
|
458
|
+
`Mistri::Workspace::Single` opts in only when given `synchronize:`. That
|
|
459
|
+
callback must cover the complete current read, revision check, write, and
|
|
460
|
+
committed read for every competing writer. It must also refuse an already-open
|
|
461
|
+
Active Record transaction when it uses an Active Record lock. Without the
|
|
462
|
+
callback, Single keeps its legacy four-method behavior.
|
|
463
|
+
|
|
464
|
+
## MCP changes
|
|
465
|
+
|
|
466
|
+
### Existing generated OAuth models
|
|
467
|
+
|
|
468
|
+
The 0.5 Rails MCP generator stored `token_endpoint`. The current flow persists
|
|
469
|
+
the authorization-server `issuer` and rediscovers the token endpoint from that
|
|
470
|
+
exact identity before code exchange and refresh.
|
|
471
|
+
|
|
472
|
+
For an existing generated model:
|
|
473
|
+
|
|
474
|
+
1. Add an `issuer` String column.
|
|
475
|
+
2. Persist `flow["issuer"]` in `connect` instead of trusting
|
|
476
|
+
`flow["token_endpoint"]`.
|
|
477
|
+
3. Pass `issuer:` to `OAuth.complete` and `OAuth.refresh`.
|
|
478
|
+
4. Keep and pass `token_auth_method`.
|
|
479
|
+
5. Add one class-owned `mcp_allow_non_public` policy and pass it to
|
|
480
|
+
`OAuth.start`, `.complete`, `.refresh`, and `MCP::Client` when internal
|
|
481
|
+
destinations are allowed.
|
|
482
|
+
6. Restart pending OAuth flows.
|
|
483
|
+
7. Reconnect existing rows unless the application independently recorded the
|
|
484
|
+
exact issuer used during registration.
|
|
485
|
+
|
|
486
|
+
Mistri no longer adds `offline_access`; include it explicitly in `scope:` only
|
|
487
|
+
when host policy and the authorization server require it. The former implicit
|
|
488
|
+
localhost allowance is also gone. Loopback HTTP development now needs an
|
|
489
|
+
explicit `allow_non_public: ->(_uri, address) { address.loopback? }` through the
|
|
490
|
+
complete flow. A pre-registered client, or discovery advertising multiple
|
|
491
|
+
authorization servers, requires the exact trusted `issuer:`.
|
|
492
|
+
|
|
493
|
+
Never infer an issuer from a token endpoint. They are different security
|
|
494
|
+
identities. An unused legacy `token_endpoint` column may be removed in a later
|
|
495
|
+
host migration after rollback needs have passed.
|
|
496
|
+
|
|
497
|
+
Compare the generated files in
|
|
498
|
+
[`lib/generators/mistri/mcp/templates/`](lib/generators/mistri/mcp/templates)
|
|
499
|
+
with the host copy. Updating the gem never rewrites generated application code.
|
|
500
|
+
|
|
501
|
+
### Network policy
|
|
502
|
+
|
|
503
|
+
Remote MCP and server-side OAuth now default to public HTTPS, validated DNS,
|
|
504
|
+
direct connections, no redirects, and no ambient proxy. An internal server must
|
|
505
|
+
receive the same narrow `allow_non_public:` callback through the entire flow.
|
|
506
|
+
|
|
507
|
+
Custom headers are copied when the Client is constructed, and transport-owned
|
|
508
|
+
framing, MCP session, and protocol names are rejected. Use the token callable for
|
|
509
|
+
rotating authorization. Deployments that require an ambient forward proxy or
|
|
510
|
+
HTTP redirects need a host-supplied client; the built-in Client deliberately
|
|
511
|
+
connects directly.
|
|
512
|
+
|
|
513
|
+
### Response limits and delivery
|
|
514
|
+
|
|
515
|
+
JSON bodies, individual SSE lines, and stdio records default to 8 MiB per
|
|
516
|
+
record. Set a larger `max_record_bytes:` only for a trusted server after
|
|
517
|
+
reviewing the memory boundary.
|
|
518
|
+
|
|
519
|
+
Provider records use `provider_options: { max_record_bytes: ... }`; the MCP
|
|
520
|
+
Client takes `max_record_bytes:` directly. Servers exceeding the new default of
|
|
521
|
+
100 discovery pages or 10,000 tools must set reviewed `max_tool_pages:` and
|
|
522
|
+
`max_tools:` values explicitly.
|
|
523
|
+
|
|
524
|
+
An unconfirmed `tools/call` response now raises
|
|
525
|
+
`Mistri::AmbiguousDeliveryError` and is never replayed automatically. Catch it
|
|
526
|
+
when calling the Client directly, mark the operation for reconciliation, and
|
|
527
|
+
verify remote state before deciding whether a new call is safe. A Client used
|
|
528
|
+
through `Mistri::MCP.tools` raises inside the tool executor, which returns the
|
|
529
|
+
same uncertainty as an error-marked tool result. The transport still does not
|
|
530
|
+
replay it, but the model may choose to issue a new call.
|
|
531
|
+
|
|
532
|
+
### MCP schemas
|
|
533
|
+
|
|
534
|
+
Mistri enforces directly reachable portable assertions and lets the MCP server
|
|
535
|
+
enforce standard assertions outside that subset. Common map schemas with
|
|
536
|
+
schema-valued `additionalProperties` work in core. Use a complete validator only
|
|
537
|
+
when local policy depends on the whole schema.
|
|
538
|
+
|
|
539
|
+
An explicit `inputSchema: null` now rejects. Omit `inputSchema` for a closed
|
|
540
|
+
no-argument tool, or provide an object schema.
|
|
541
|
+
|
|
542
|
+
See [MCP](docs/mcp.md).
|
|
543
|
+
|
|
544
|
+
## Cost budgets and pricing
|
|
545
|
+
|
|
546
|
+
0.5 exposed a cost budget but did not calculate provider cost, so the comparison
|
|
547
|
+
effectively saw zero. 0.6 prices catalogued standard paid direct-API usage and
|
|
548
|
+
represents unknown cost as unknown.
|
|
549
|
+
|
|
550
|
+
A `cost_usd` budget now fails at construction unless the model, origin, and
|
|
551
|
+
service-tier policy can produce deterministic catalog pricing. Configure:
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# Anthropic
|
|
555
|
+
anthropic_options = { service_tier: "standard_only" }
|
|
556
|
+
|
|
557
|
+
# OpenAI
|
|
558
|
+
openai_options = { service_tier: "default" }
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Gemini's omitted tier is standard. Flex, Priority, an unreported nonstandard
|
|
562
|
+
tier, an unknown model, or a custom origin remains unpriced unless the custom
|
|
563
|
+
provider supplies complete known cost.
|
|
564
|
+
|
|
565
|
+
Without a cost budget, unknown models and custom origins continue to work.
|
|
566
|
+
Inspect `result.usage.cost.known?` before presenting an estimate.
|
|
567
|
+
|
|
568
|
+
Budgets remain soft ceilings checked between model-visible turns. A final turn
|
|
569
|
+
and its retries may cross the configured amount.
|
|
570
|
+
|
|
571
|
+
## Compaction behavior
|
|
572
|
+
|
|
573
|
+
Catalogued Anthropic and OpenAI windows now use their published long-context
|
|
574
|
+
limits. Automatic reserve protects the model's full shared output capacity plus
|
|
575
|
+
framing slack; Gemini keeps its separately published input-oriented reserve.
|
|
576
|
+
|
|
577
|
+
Sessions with default compaction will therefore compact at different and more
|
|
578
|
+
accurate boundaries. An explicit `reserve:` remains host policy and wins.
|
|
579
|
+
|
|
580
|
+
Compaction can occur more than once inside one run while preserving tool-call
|
|
581
|
+
pairing. The full entry log remains exact; replay after a successful compaction
|
|
582
|
+
intentionally becomes the visible summary plus kept tail.
|
|
583
|
+
|
|
584
|
+
Review any UI or alerting code that assumed the old early thresholds. Only the
|
|
585
|
+
`:compaction` event carries the summary; `:compacting` is the start signal.
|
|
586
|
+
|
|
587
|
+
## Provider refusal and stream behavior
|
|
588
|
+
|
|
589
|
+
Documented provider refusals, content-policy stops, and invalid requests now
|
|
590
|
+
fail fast instead of looking like clean completion or consuming retries on the
|
|
591
|
+
same rejected input. Unknown transient stream failures still follow the retry
|
|
592
|
+
policy.
|
|
593
|
+
|
|
594
|
+
Interrupted tool calls and partial Anthropic thinking signatures are removed
|
|
595
|
+
before persistence. Built-in stream assemblers now enforce known lifecycle,
|
|
596
|
+
index, and terminal-record invariants while ignoring unknown future event types
|
|
597
|
+
for forward compatibility.
|
|
598
|
+
|
|
599
|
+
If host code classified provider errors by prose, move to the typed
|
|
600
|
+
`result.errored?`, `result.message.error`, `result.stop_reason`, and structured
|
|
601
|
+
event or message fields. Built-in provider refusals finish in the Result
|
|
602
|
+
contract; they are not raised as ordinary transport exceptions.
|
|
603
|
+
|
|
604
|
+
## Directory workspaces
|
|
605
|
+
|
|
606
|
+
Directory workspaces no longer follow existing symlinks in model-controlled
|
|
607
|
+
paths and omit symlinked entries from listings. Replace intentional links with
|
|
608
|
+
host-controlled tools or real files inside the workspace root. This boundary is
|
|
609
|
+
deliberate and has no opt-out in the model-controlled file tools.
|
|
610
|
+
|
|
611
|
+
## Verification before production
|
|
612
|
+
|
|
613
|
+
At minimum:
|
|
614
|
+
|
|
615
|
+
1. Run the hermetic suite and application tests.
|
|
616
|
+
2. Audit active persisted sessions with `tool_control_state`.
|
|
617
|
+
3. Resume a 0.5 Anthropic session with tool calls and reasoning.
|
|
618
|
+
4. Resume a 0.5 Gemini session containing tool calls from more than one turn.
|
|
619
|
+
5. Exercise one approval grant and denial after rebuilding current policy.
|
|
620
|
+
6. Run a write tool with an idempotency key and simulate failure before result
|
|
621
|
+
persistence in the host test harness.
|
|
622
|
+
7. Reconnect and refresh one MCP OAuth connection.
|
|
623
|
+
8. For a workspace that requires concurrent edit preservation, assert
|
|
624
|
+
`atomic_writes?` and race two anchored edits against its real backend.
|
|
625
|
+
9. Run the provider-specific live tests and integration matrix with every
|
|
626
|
+
expected key present.
|
|
627
|
+
|
|
628
|
+
```console
|
|
629
|
+
$ bundle exec rake test
|
|
630
|
+
$ bundle exec rubocop
|
|
631
|
+
$ MISTRI_LIVE=1 bundle exec rake test
|
|
632
|
+
$ MISTRI_INTEGRATION_STRICT=1 bundle exec rake integration
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Ordinary integration runs skip missing provider keys and an unreachable public
|
|
636
|
+
MCP fixture. Strict mode fails when a matrix key is absent or that fixture is
|
|
637
|
+
unavailable, so use it for release verification.
|
|
638
|
+
|
|
639
|
+
For exact behavior and latency notes, read the
|
|
640
|
+
[0.6.0 changelog entry](CHANGELOG.md#060---2026-07-12).
|