synartesis 0.6.11 → 0.6.13

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 CHANGED
@@ -2,6 +2,206 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.6.13 — 2026-09-14
6
+
7
+ Four things, all of them from one comment by a stranger on Reddit who had
8
+ clearly built something like this before. Every one was real, and none of them
9
+ would have announced itself.
10
+
11
+ ### Security
12
+
13
+ - **The one remaining advisory is gone.** `esbuild` reached `pnpm audit`
14
+ through vite, in the renderer build only -- never in anything published, and
15
+ `--prod` was already clean. Pinned anyway, the same way as 0.6.12's three:
16
+ a low advisory nobody can reach is still a line of output that trains you to
17
+ ignore the tool.
18
+
19
+ - **A server upgrade could silently invalidate the policy written for it.** A
20
+ policy is a claim about what a tool does, anchored to the tool's name -- and
21
+ a name is a weak anchor. A server can keep `write_file` and change what it
22
+ takes. The policy still says reversible, the snapshot still reads a field
23
+ that has moved, and the before-image captured no longer matches the write.
24
+ Nothing failed. The undo was produced later, on request, confidently, and was
25
+ wrong, which is worse than having no undo because somebody acted on it.
26
+
27
+ Startup already checked that the tools a policy names exist. It did not check
28
+ that they still have the shape the policy was written for.
29
+
30
+ A manifest may now pin that shape:
31
+
32
+ ```yaml
33
+ pins:
34
+ fs:
35
+ write_file: "sha256:ce17c85e8a58835..."
36
+ ```
37
+
38
+ `synartesis pin` prints the block for the servers you actually have. With it
39
+ in place, a tool whose shape has moved stops the proxy at startup and names
40
+ both fingerprints, instead of being quietly trusted.
41
+
42
+ It prints rather than rewriting the manifest: pinning is a person vouching
43
+ for what a tool does today, and a command that edited the policy for them
44
+ would let that happen with nobody reading it.
45
+
46
+ Pinning is per server and all-or-nothing. No pins means no checking, so every
47
+ manifest written before this keeps working. Any pins means that server is
48
+ checked in full -- a half-pinned server is the worst of both, because it
49
+ reads as protected and is not. Tools no policy matches need no pin; they are
50
+ already fail-closed as irreversible and gated.
51
+
52
+ ### Fixed
53
+
54
+ - **Forward calls carried no idempotency key.** The key was minted for every
55
+ action and stored, and it was presented on the inverse -- but not on the
56
+ call going out. So a write that timed out in flight left the agent free to
57
+ retry, with nothing telling the server that the retry was the same intention.
58
+ Two side effects would sit behind one journal row, and undo would reverse one
59
+ of them and report success.
60
+
61
+ The key now rides out with the forward call as well, merged into `_meta`
62
+ rather than replacing it, so a client's own `progressToken` survives. It
63
+ remains advisory -- a server that ignores it gives no protection, which is
64
+ why the journal's state transitions are still the real guard.
65
+
66
+ - **A compensable action could be undone over somebody's work, silently.** An
67
+ action whose undo is a compensating call -- a create offset by a delete --
68
+ has no before-image, because the thing did not exist before the call. So
69
+ there was nothing for undo to compare against: it compensated regardless and
70
+ marked the step `[unverified]`.
71
+
72
+ A policy may now declare a `verify` read, resolved *after* the call so it can
73
+ name a resource the call itself created:
74
+
75
+ ```yaml
76
+ verify:
77
+ tool: "memory.open_nodes"
78
+ args: { names: "$result.entities[].name" }
79
+ ```
80
+
81
+ Undo then halts on drift the way it does everywhere else. Measured against
82
+ the real `@modelcontextprotocol/server-memory`: an agent creates an entity, a
83
+ person adds an observation to it by hand, and undo is asked for. Before, it
84
+ deleted the entity, took the hand-written observation with it, and reported
85
+ `rolled_back`. Now it halts, prints the added line, and writes nothing.
86
+
87
+ It is consulted only where no read exists already, so it can never displace a
88
+ working pre-read with a differently shaped one -- which would leave the
89
+ post-state and the snapshot describing different things and make every later
90
+ comparison meaningless.
91
+
92
+ `memory.create_entities` gets one. Deleting an entity takes its observations
93
+ and relations with it, which is exactly the case worth refusing.
94
+
95
+ - **Nothing said which policies had actually been run against a real server.**
96
+ Three of the four that ship were written against the live server; `github`
97
+ never has been, and its own header has said so in plain words since it was
98
+ written. Nothing in the code read that header. `check` did not mention it,
99
+ `install` adopted the policy without a word, and a held GitHub call looked
100
+ exactly as confident as a held filesystem one.
101
+
102
+ A server may now state it, and the four that ship do:
103
+
104
+ ```yaml
105
+ servers:
106
+ gh:
107
+ command: github-mcp-server
108
+ provenance: documented # or: live
109
+ ```
110
+
111
+ Surfaced by `check`, by the proxy at every start -- before it connects, since
112
+ the untried adapter is the one whose server is least likely to be installed
113
+ -- and by `install` at the moment the policy is adopted. `init` writes the
114
+ claim into the manifest it generates, or the warning would go quiet exactly
115
+ when the policy starts being used.
116
+
117
+ Absent means no claim either way, which is right for a policy somebody wrote
118
+ themselves. All three states are printed: if silence meant "fine", an
119
+ ungraded policy and a known-untested one would look identical from here.
120
+
121
+ - **A call somebody approved was reported as one they had refused.** When an
122
+ agent retries a held call, the approval moves onto the row that actually
123
+ runs and the original is retired -- stored as `denied`, with an error saying
124
+ where its approval went. `labelFor` existed to keep that out of the UI, and
125
+ three readers did not use it. `show` printed "denied by <name>" beside a call
126
+ that person had just approved and which had gone through; its footer counted
127
+ the row as a refusal; and `show --live` called it "never applied (denied)".
128
+ All three now go through `labelFor`, and the live view says what actually
129
+ happened: "its approval moved to the call that ran".
130
+
131
+ - **Arguments are summarised for a terminal again.** A value with a newline in
132
+ it was printed verbatim, so writing two lines to a file broke the timeline
133
+ and left the second at column zero. A value over 48 characters was reduced to
134
+ a byte count -- which for a path is the one summary that answers nothing, so
135
+ every row of a filesystem session read `path 120 B` and named no file.
136
+ Values are flattened to one line, a long path keeps its end, and long prose
137
+ keeps its size, because the tail of a file tells you nothing.
138
+
139
+ The undo plan used raw truncated json for the same job and cut off mid-path;
140
+ it uses the same summary now, so a step reads
141
+ `would call fs.write_file path …/work/ledger.csv content north,412800`.
142
+
143
+ - **`synartesis close` treated a tidy journal as a usage error.** Nothing left
144
+ open is the ordinary state and the thing somebody runs the command to check.
145
+ It answered with exit 2 and forty lines of unrelated help. It now says
146
+ "nothing is open" and succeeds.
147
+
148
+ Separately, "there is no run to act on" and "no run matches <id>" stopped
149
+ reciting every command. They are facts about the journal rather than about
150
+ what was typed, and the command list buried the one sentence that mattered.
151
+ A mistyped command still gets the full list.
152
+
153
+ - **`-v`, `version` and `help` answer.** `--version` worked and `synartesis
154
+ version` said "unknown command", which is a riddle rather than an answer.
155
+
156
+ - **Listing sessions read every action of every run.** It needs a count and
157
+ three status tallies per run, and it was getting them by materialising every
158
+ row -- snapshots, results and inverses included, which is the bulk of the
159
+ table. So the cost of listing sessions grew with the size of the data those
160
+ sessions had touched rather than with how many there were. It is one grouped
161
+ query now, over a covering index. Measured on forty runs of five hundred
162
+ actions with two-kilobyte snapshots, a hundred-megabyte journal: **76ms to
163
+ 2ms**.
164
+
165
+ The index is added the same way as the two in 0.6.12 and for the same reason:
166
+ no row changes, no meaning changes, and an older build opening the same file
167
+ neither notices nor cares.
168
+
169
+ - `synartesis check` now says whether anything is pinned, either way. Silence
170
+ when nothing was would have left the safer state and the unchecked one
171
+ looking identical.
172
+
173
+ ## 0.6.12 — 2026-09-13
174
+
175
+ ### Security
176
+
177
+ - **Nine advisories, none of them ours, all of them shipped anyway.** Four
178
+ high and five moderate arrived through one dependency's dependencies --
179
+ `fast-uri`, `qs` and `hono`, by way of the MCP SDK. Nothing in the published
180
+ npm package contained them: that bundle imports the SDK rather than inlining
181
+ it, so a fresh install resolves patched versions on its own. The desktop
182
+ application is the opposite -- it bundles everything, so it was carrying
183
+ `fast-uri` 3.1.5 and its four high advisories into every installer. Pinned to
184
+ patched versions; `pnpm audit --prod` now reports nothing.
185
+
186
+ ### Fixed
187
+
188
+ - **Two lookups that ran while somebody was waiting were full table scans.**
189
+ Finding a standing approval happens twice on every call that needs one, and
190
+ listing what is held backs both `synartesis gates` and the console. Neither
191
+ had an index, so both read every row in the journal -- including the
192
+ snapshots, which are the largest column there. At fifty thousand actions with
193
+ two-kilobyte snapshots, measured: 61ms and 56ms, on every irreversible call,
194
+ growing with the journal.
195
+
196
+ Both are now indexed: **61ms to 0.01ms, and 56ms to nothing.**
197
+
198
+ Deliberately without a schema version bump. This build refuses to open a
199
+ journal written by a different schema, so bumping would have told everybody
200
+ with history to abandon it in exchange for an index. Adding an index changes
201
+ no row and no meaning, the statement is idempotent, and it runs on every
202
+ open -- so an existing journal gains both the next time it is opened, and an
203
+ older build opening the same file afterwards neither notices nor cares.
204
+
5
205
  ## 0.6.11 — 2026-09-13
6
206
 
7
207
  ### Fixed
package/README.md CHANGED
@@ -179,6 +179,103 @@ answers. `l` in the screen does the same.
179
179
  If you decide the recorded value is the one worth keeping, `undo --force` prints
180
180
  every line it would write over and stops; `--force --yes` goes ahead.
181
181
 
182
+ ## What each policy has actually been tested against
183
+
184
+ A policy that has met a real server and one written from its documentation are
185
+ not the same kind of claim, and the difference only shows up at the moment
186
+ somebody needs undo to work. So a policy can say which it is:
187
+
188
+ ```yaml
189
+ servers:
190
+ gh:
191
+ command: github-mcp-server
192
+ provenance: documented # or: live
193
+ ```
194
+
195
+ Of the four that ship, three say `live` — they were written against the real
196
+ server and corrected where it disagreed with its own docs. **`github` says
197
+ `documented`**: it has never been run against a real account, and its own header
198
+ has always said so. Now `check` says it, the proxy says it at every start, and
199
+ `install` says it at the moment the policy is adopted — rather than leaving it in
200
+ a file for you to find afterwards.
201
+
202
+ Absent means no claim either way, which is the right default for a policy you
203
+ wrote yourself: the tool has no business grading your work. Nothing is inferred
204
+ from silence, and all three states are printed, because if silence meant "fine"
205
+ then an ungraded policy and a known-untested one would look identical.
206
+
207
+ ## Undoing something that was never read first
208
+
209
+ Most undo rides on a pre-read: the value before the write is captured, and
210
+ before putting it back, undo reads the world again and refuses if it has moved.
211
+
212
+ A compensable action has no such read. `create_entities` makes something that
213
+ did not exist a moment earlier, so there is nothing to capture — it has a
214
+ compensating action instead, a delete that offsets the create. Which means undo
215
+ had nothing to compare against and compensated regardless. If you had added to
216
+ that record in the meantime, the delete took your work with it and the run
217
+ reported success.
218
+
219
+ A policy can now declare a read used only for that check:
220
+
221
+ ```yaml
222
+ - match: "memory.create_entities"
223
+ class: compensable
224
+ inverse:
225
+ tool: "memory.delete_entities"
226
+ args: { entityNames: "$result.entities[].name" }
227
+ verify:
228
+ tool: "memory.open_nodes"
229
+ args: { names: "$result.entities[].name" }
230
+ ```
231
+
232
+ It is resolved *after* the call, so `$result` is available and it can name a
233
+ resource the call itself created. Undo then halts on drift the same way it does
234
+ everywhere else, and shows you the diff.
235
+
236
+ It is consulted only where there is no read already, so it can never displace a
237
+ working pre-read with a differently shaped one — which would make the post-state
238
+ and the snapshot incomparable and every later comparison meaningless.
239
+
240
+ ## When the server changes underneath you
241
+
242
+ A policy is a claim about what a tool does, and a tool's name is a weak place to
243
+ anchor that claim. A server upgrade can keep `write_file` and add an argument to
244
+ it. The policy still says reversible, the snapshot still reads a field that has
245
+ moved, and the before-image captured no longer matches the write. Nothing fails.
246
+ The undo is produced on request, confidently, and is wrong — which is worse than
247
+ having no undo, because somebody acted on it.
248
+
249
+ So you can pin the shape a tool had when you wrote its policy:
250
+
251
+ ```bash
252
+ synartesis pin
253
+ ```
254
+
255
+ It prints a block. Paste it into the manifest:
256
+
257
+ ```yaml
258
+ pins:
259
+ fs:
260
+ write_file: "sha256:ce17c85e8a5883552a11555f9b893de497fadab965a5c7935c0cb8f3c55b91d6"
261
+ edit_file: "sha256:88459ef670b139a12a3e0335ae0a4584dd892f60f45f565b545e1004d7565dd5"
262
+ ```
263
+
264
+ From then on, a tool whose shape has moved stops the proxy at startup and names
265
+ both fingerprints, instead of quietly serving the old policy. Re-run `pin` when
266
+ you have looked at what changed and decided the policy still holds.
267
+
268
+ It prints rather than writes on purpose: pinning is you vouching for what a tool
269
+ does today, and a command that silently rewrote your policy would let that happen
270
+ without anyone reading it.
271
+
272
+ Pinning is per server and all-or-nothing. A server with no pins is not checked,
273
+ so every manifest written before this existed keeps working. A server with any
274
+ pins is checked in full — a half-pinned server is the worst of both, because it
275
+ reads as protected and is not. Tools that no policy matches need no pin: they are
276
+ already fail-closed as irreversible and gated, so there is no classification for a
277
+ schema change to corrupt.
278
+
182
279
  ## Commands
183
280
 
184
281
  `synartesis desktop` opens [the window](#the-desktop-window), and says where to
@@ -190,6 +287,7 @@ get it if it is not installed.
190
287
  | `install` / `uninstall` / `status` | Cover the clients on this machine, put them back, say what is covered |
191
288
  | `init <server> -- <cmd>` | Introspect a server and draft a manifest |
192
289
  | `check` | Load a manifest and verify it against the servers it names |
290
+ | `pin` | Print the `pins:` block for the servers you have now |
193
291
  | `list` | Every recorded session |
194
292
  | `show <id>` | One session's timeline, with the undo for each step |
195
293
  | `show <id> --live` | The same, plus what has changed in the world since |
@@ -218,8 +316,11 @@ cannot start a process:** see the [user guide](docs/synartesis-user-guide.md).
218
316
 
219
317
  - **It cannot un-send what has been seen.** An email that has been read, a
220
318
  posted message, a file deleted with no backup. This is why the gate exists.
221
- - **Compensable actions cannot be checked for drift.** They declare no pre-read,
222
- so undo compensates them and marks them `[unverified]`.
319
+ - **Compensable actions can only be checked for drift if their policy declares a
320
+ `verify` read.** They have no pre-read — the thing they made did not exist
321
+ before the call — so without one, undo compensates them and marks them
322
+ `[unverified]`. With one, the resource is read back after the write and undo
323
+ halts rather than compensating over somebody else's edit.
223
324
  - **Undo halts on uncertainty, and steps over the merely permanent.** Drift, an
224
325
  unknown outcome, or a failed reversing call stop it. An action that simply
225
326
  cannot be undone is reported and left in place while everything else is