synartesis 0.6.12 → 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 +168 -0
- package/README.md +103 -2
- package/dist/{chunk-FUVEHRJP.js → chunk-6X7GPUGJ.js} +231 -46
- package/dist/cli.js +192 -43
- package/dist/proxy.js +21 -3
- package/manifests/filesystem.yaml +8 -0
- package/manifests/git.yaml +8 -0
- package/manifests/github.yaml +10 -0
- package/manifests/memory.yaml +18 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,174 @@
|
|
|
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
|
+
|
|
5
173
|
## 0.6.12 — 2026-09-13
|
|
6
174
|
|
|
7
175
|
### Security
|
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
|
|
222
|
-
|
|
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
|