synartesis 0.6.12 → 0.6.15
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 +308 -0
- package/README.md +103 -2
- package/dist/{chunk-FUVEHRJP.js → chunk-OODDXM36.js} +376 -66
- package/dist/cli.js +511 -88
- package/dist/proxy.js +39 -9
- 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,314 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.6.15 — 2026-09-15
|
|
6
|
+
|
|
7
|
+
One gap, named by a stranger on Reddit and real.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **A retry of a call whose outcome nobody could establish is now held for a
|
|
12
|
+
person.** The tri-state was already there -- a write that times out is not
|
|
13
|
+
called `failed`, the resource is re-read to see which way it went, and what
|
|
14
|
+
cannot be settled is left `pending`, which undo refuses to step over. What
|
|
15
|
+
was missing was the other half: nothing stopped the agent from simply making
|
|
16
|
+
the call again.
|
|
17
|
+
|
|
18
|
+
The idempotency key cannot close this. It is `runId:seq`, so a retry is a new
|
|
19
|
+
row under a different key and no upstream can tell the two attempts were one
|
|
20
|
+
intention -- checked against the real proxy, which records `seq=1 key=…:1`
|
|
21
|
+
and `seq=2 key=…:2`. A comment beside the forward call claimed otherwise and
|
|
22
|
+
has been corrected.
|
|
23
|
+
|
|
24
|
+
So the person who can go and look is asked before a second side effect
|
|
25
|
+
exists. It reuses the gate rather than inventing a second mechanism: the call
|
|
26
|
+
appears in `synartesis gates` with a reason that says what happened, rather
|
|
27
|
+
than the generic policy text.
|
|
28
|
+
|
|
29
|
+
Three things it deliberately does not do. It never holds a **read** -- a read
|
|
30
|
+
that timed out is left `pending` like anything else, but reading twice
|
|
31
|
+
changes nothing, and an agent may always look freely. It holds only the
|
|
32
|
+
**identical** call, matched on server, tool and arguments: the uncertainty is
|
|
33
|
+
about one call, not about the tool. And approving the retry **does not
|
|
34
|
+
resolve the first attempt** -- that row stays `pending` and undo still stops
|
|
35
|
+
on it, because saying yes to going forward is not a claim about what already
|
|
36
|
+
happened.
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
- `findPending` runs on the way in to every write, and left to choose sqlite
|
|
41
|
+
walked every action in the run -- rows that carry the snapshots -- to find
|
|
42
|
+
the handful that are pending. A partial index over exactly those rows, named
|
|
43
|
+
explicitly, on a single run of twenty thousand actions with two-kilobyte
|
|
44
|
+
snapshots: **24ms a call to 0.004ms**.
|
|
45
|
+
|
|
46
|
+
## 0.6.14 — 2026-09-15 (tagged, never published)
|
|
47
|
+
|
|
48
|
+
The terminal now says what to do next, and the argument parser stopped
|
|
49
|
+
disagreeing with itself.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **Most commands end by naming the one thing worth doing next**, with the
|
|
54
|
+
session id already filled in:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
fs.create_directory is held, and the agent is waiting on you: synartesis approve f0c9935d
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It is worked out from the journal rather than from what was typed, so it
|
|
61
|
+
changes as the state does. A call held for approval outranks everything else
|
|
62
|
+
-- that is an agent stopped mid-task waiting on a person who may not know it
|
|
63
|
+
is waiting. Below that: the session that changed something, what is not
|
|
64
|
+
pinned, and `watch`. There is at most one, there is none when nothing
|
|
65
|
+
applies, and `--json` never carries it. `SYNARTESIS_NO_HINTS` turns it off.
|
|
66
|
+
|
|
67
|
+
The session it names is not simply the newest one. A client that connects
|
|
68
|
+
and reads opens a session like any other, so "the last thing that happened"
|
|
69
|
+
is regularly a session in which nothing happened; a session that only read,
|
|
70
|
+
or was already undone, or was held and never ran, is never offered for undo.
|
|
71
|
+
|
|
72
|
+
- **A mistyped command gets the word that was meant.** `synartesis lst` used to
|
|
73
|
+
answer with forty lines of help, thirty-nine of them about something the
|
|
74
|
+
person was not doing. It now says `did you mean list?` and lists five
|
|
75
|
+
commands. Flags too: `--jounral`, `--dryrun`, `--forse` all resolve.
|
|
76
|
+
|
|
77
|
+
- **A flag that belongs to `proxy` is named as such.** `--http`, `--token`,
|
|
78
|
+
`--server` and `--log-level` are in the help page, and answering one of them
|
|
79
|
+
with "unknown flag" sent people hunting through that page for a flag already
|
|
80
|
+
in it.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **A wrapped server's own flags were read as ours.** `synartesis init db --
|
|
85
|
+
some-server --manifest audit.yaml` handed `--manifest` to the server
|
|
86
|
+
correctly and *also* took it, writing the policy to a path nobody asked for
|
|
87
|
+
-- on a machine where that path is real, one they already had. `positional`
|
|
88
|
+
and the unknown-flag check had always stopped at the bare `--`; the flag
|
|
89
|
+
reader had not.
|
|
90
|
+
|
|
91
|
+
- **The value of a flag was read as a flag.** `deny --reason "-see ticket 42"`
|
|
92
|
+
answered `unknown flag -see ticket 42`, and `prune --older-than -5` answered
|
|
93
|
+
`unknown flag -5` instead of the message that knows what `--older-than` is
|
|
94
|
+
for. Three separate lists had to agree about which flags take a value and
|
|
95
|
+
did not; there is one now, and a test holds it against the help page.
|
|
96
|
+
|
|
97
|
+
- **`undo --to 0` announced the session before refusing.** It printed "no
|
|
98
|
+
session named, so the most recent: a42bf93a" and only then rejected the
|
|
99
|
+
flag, which reads as though that session had been acted on.
|
|
100
|
+
|
|
101
|
+
- **A hint that could not be pasted.** `list --journal ./bench.db` named a
|
|
102
|
+
session, and the `show` it offered read the *default* journal and answered
|
|
103
|
+
"no run matches". Hints now carry `--journal` and `--manifest` when those
|
|
104
|
+
were not the obvious ones, and `undo --replan --dry-run` offers a command
|
|
105
|
+
that still replans.
|
|
106
|
+
|
|
107
|
+
- **Guessing at a typo was too eager.** A ceiling of half the word answered
|
|
108
|
+
`--server` with `--live` and `--token` with `--to`, neither of which anybody
|
|
109
|
+
meant. Two edits at most now, never for a word under three characters, and a
|
|
110
|
+
swapped pair counts as the one mistake it feels like -- which is what keeps
|
|
111
|
+
`shwo`, `pruen` and `--jounral` reachable at that ceiling.
|
|
112
|
+
|
|
113
|
+
- **Colour was decided for both streams from stdout**, so `synartesis nonsense
|
|
114
|
+
2> errors.log` in a terminal wrote escape sequences into the file.
|
|
115
|
+
|
|
116
|
+
### Performance
|
|
117
|
+
|
|
118
|
+
- **`newestUndoable` runs on every `list`** and could only be answered by
|
|
119
|
+
reading the action rows, which carry the snapshots -- so the cost of one
|
|
120
|
+
screen of sessions grew with the size of the data rather than the number of
|
|
121
|
+
sessions. A partial index over exactly the rows it wants fixes that, but only
|
|
122
|
+
when named: left to choose, sqlite takes `actions_gated` and reads the rows
|
|
123
|
+
anyway. Measured on a 246MB journal: **56ms to 0.01ms**.
|
|
124
|
+
|
|
125
|
+
## 0.6.13 — 2026-09-14
|
|
126
|
+
|
|
127
|
+
Four things, all of them from one comment by a stranger on Reddit who had
|
|
128
|
+
clearly built something like this before. Every one was real, and none of them
|
|
129
|
+
would have announced itself.
|
|
130
|
+
|
|
131
|
+
### Security
|
|
132
|
+
|
|
133
|
+
- **A server that would not start said nothing useful about why, and could
|
|
134
|
+
hang the proxy outright.** Two faults in the same few lines, found by hitting
|
|
135
|
+
the first one in real use.
|
|
136
|
+
|
|
137
|
+
What it printed was `code: 'MODULE_NOT_FOUND', requireStack: [], },
|
|
138
|
+
Node.js v22.16.0` -- the last four lines of node's output, which are the four
|
|
139
|
+
least informative. `Error: Cannot find module '/path/to/thing'` sits in the
|
|
140
|
+
middle and was thrown away, so the message named neither the file nor
|
|
141
|
+
anything to act on. A line that states a fault now leads, stack frames are
|
|
142
|
+
dropped, and the tail is only the fallback when nothing states one.
|
|
143
|
+
|
|
144
|
+
The second was worse and was found while testing the first: nothing read the
|
|
145
|
+
server's stderr until after the connection failed. A pipe nobody reads fills
|
|
146
|
+
at around 64kB and blocks the writer, so a server that said more than that
|
|
147
|
+
before dying never exited, the connect never rejected, and `check` sat there
|
|
148
|
+
for ever with no output at all. Measured against a server writing 2.4MB to
|
|
149
|
+
stderr: **killed at 30 seconds, against 0.8 seconds and the right error**.
|
|
150
|
+
Stderr is now read as it arrives, capped so a logging loop cannot make the
|
|
151
|
+
proxy the thing that runs out of memory.
|
|
152
|
+
|
|
153
|
+
- **The one remaining advisory is gone.** `esbuild` reached `pnpm audit`
|
|
154
|
+
through vite, in the renderer build only -- never in anything published, and
|
|
155
|
+
`--prod` was already clean. Pinned anyway, the same way as 0.6.12's three:
|
|
156
|
+
a low advisory nobody can reach is still a line of output that trains you to
|
|
157
|
+
ignore the tool.
|
|
158
|
+
|
|
159
|
+
- **A server upgrade could silently invalidate the policy written for it.** A
|
|
160
|
+
policy is a claim about what a tool does, anchored to the tool's name -- and
|
|
161
|
+
a name is a weak anchor. A server can keep `write_file` and change what it
|
|
162
|
+
takes. The policy still says reversible, the snapshot still reads a field
|
|
163
|
+
that has moved, and the before-image captured no longer matches the write.
|
|
164
|
+
Nothing failed. The undo was produced later, on request, confidently, and was
|
|
165
|
+
wrong, which is worse than having no undo because somebody acted on it.
|
|
166
|
+
|
|
167
|
+
Startup already checked that the tools a policy names exist. It did not check
|
|
168
|
+
that they still have the shape the policy was written for.
|
|
169
|
+
|
|
170
|
+
A manifest may now pin that shape:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
pins:
|
|
174
|
+
fs:
|
|
175
|
+
write_file: "sha256:ce17c85e8a58835..."
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`synartesis pin` prints the block for the servers you actually have. With it
|
|
179
|
+
in place, a tool whose shape has moved stops the proxy at startup and names
|
|
180
|
+
both fingerprints, instead of being quietly trusted.
|
|
181
|
+
|
|
182
|
+
It prints rather than rewriting the manifest: pinning is a person vouching
|
|
183
|
+
for what a tool does today, and a command that edited the policy for them
|
|
184
|
+
would let that happen with nobody reading it.
|
|
185
|
+
|
|
186
|
+
Pinning is per server and all-or-nothing. No pins means no checking, so every
|
|
187
|
+
manifest written before this keeps working. Any pins means that server is
|
|
188
|
+
checked in full -- a half-pinned server is the worst of both, because it
|
|
189
|
+
reads as protected and is not. Tools no policy matches need no pin; they are
|
|
190
|
+
already fail-closed as irreversible and gated.
|
|
191
|
+
|
|
192
|
+
### Fixed
|
|
193
|
+
|
|
194
|
+
- **Forward calls carried no idempotency key.** The key was minted for every
|
|
195
|
+
action and stored, and it was presented on the inverse -- but not on the
|
|
196
|
+
call going out. So a write that timed out in flight left the agent free to
|
|
197
|
+
retry, with nothing telling the server that the retry was the same intention.
|
|
198
|
+
Two side effects would sit behind one journal row, and undo would reverse one
|
|
199
|
+
of them and report success.
|
|
200
|
+
|
|
201
|
+
The key now rides out with the forward call as well, merged into `_meta`
|
|
202
|
+
rather than replacing it, so a client's own `progressToken` survives. It
|
|
203
|
+
remains advisory -- a server that ignores it gives no protection, which is
|
|
204
|
+
why the journal's state transitions are still the real guard.
|
|
205
|
+
|
|
206
|
+
- **A compensable action could be undone over somebody's work, silently.** An
|
|
207
|
+
action whose undo is a compensating call -- a create offset by a delete --
|
|
208
|
+
has no before-image, because the thing did not exist before the call. So
|
|
209
|
+
there was nothing for undo to compare against: it compensated regardless and
|
|
210
|
+
marked the step `[unverified]`.
|
|
211
|
+
|
|
212
|
+
A policy may now declare a `verify` read, resolved *after* the call so it can
|
|
213
|
+
name a resource the call itself created:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
verify:
|
|
217
|
+
tool: "memory.open_nodes"
|
|
218
|
+
args: { names: "$result.entities[].name" }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Undo then halts on drift the way it does everywhere else. Measured against
|
|
222
|
+
the real `@modelcontextprotocol/server-memory`: an agent creates an entity, a
|
|
223
|
+
person adds an observation to it by hand, and undo is asked for. Before, it
|
|
224
|
+
deleted the entity, took the hand-written observation with it, and reported
|
|
225
|
+
`rolled_back`. Now it halts, prints the added line, and writes nothing.
|
|
226
|
+
|
|
227
|
+
It is consulted only where no read exists already, so it can never displace a
|
|
228
|
+
working pre-read with a differently shaped one -- which would leave the
|
|
229
|
+
post-state and the snapshot describing different things and make every later
|
|
230
|
+
comparison meaningless.
|
|
231
|
+
|
|
232
|
+
`memory.create_entities` gets one. Deleting an entity takes its observations
|
|
233
|
+
and relations with it, which is exactly the case worth refusing.
|
|
234
|
+
|
|
235
|
+
- **Nothing said which policies had actually been run against a real server.**
|
|
236
|
+
Three of the four that ship were written against the live server; `github`
|
|
237
|
+
never has been, and its own header has said so in plain words since it was
|
|
238
|
+
written. Nothing in the code read that header. `check` did not mention it,
|
|
239
|
+
`install` adopted the policy without a word, and a held GitHub call looked
|
|
240
|
+
exactly as confident as a held filesystem one.
|
|
241
|
+
|
|
242
|
+
A server may now state it, and the four that ship do:
|
|
243
|
+
|
|
244
|
+
```yaml
|
|
245
|
+
servers:
|
|
246
|
+
gh:
|
|
247
|
+
command: github-mcp-server
|
|
248
|
+
provenance: documented # or: live
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Surfaced by `check`, by the proxy at every start -- before it connects, since
|
|
252
|
+
the untried adapter is the one whose server is least likely to be installed
|
|
253
|
+
-- and by `install` at the moment the policy is adopted. `init` writes the
|
|
254
|
+
claim into the manifest it generates, or the warning would go quiet exactly
|
|
255
|
+
when the policy starts being used.
|
|
256
|
+
|
|
257
|
+
Absent means no claim either way, which is right for a policy somebody wrote
|
|
258
|
+
themselves. All three states are printed: if silence meant "fine", an
|
|
259
|
+
ungraded policy and a known-untested one would look identical from here.
|
|
260
|
+
|
|
261
|
+
- **A call somebody approved was reported as one they had refused.** When an
|
|
262
|
+
agent retries a held call, the approval moves onto the row that actually
|
|
263
|
+
runs and the original is retired -- stored as `denied`, with an error saying
|
|
264
|
+
where its approval went. `labelFor` existed to keep that out of the UI, and
|
|
265
|
+
three readers did not use it. `show` printed "denied by <name>" beside a call
|
|
266
|
+
that person had just approved and which had gone through; its footer counted
|
|
267
|
+
the row as a refusal; and `show --live` called it "never applied (denied)".
|
|
268
|
+
All three now go through `labelFor`, and the live view says what actually
|
|
269
|
+
happened: "its approval moved to the call that ran".
|
|
270
|
+
|
|
271
|
+
- **Arguments are summarised for a terminal again.** A value with a newline in
|
|
272
|
+
it was printed verbatim, so writing two lines to a file broke the timeline
|
|
273
|
+
and left the second at column zero. A value over 48 characters was reduced to
|
|
274
|
+
a byte count -- which for a path is the one summary that answers nothing, so
|
|
275
|
+
every row of a filesystem session read `path 120 B` and named no file.
|
|
276
|
+
Values are flattened to one line, a long path keeps its end, and long prose
|
|
277
|
+
keeps its size, because the tail of a file tells you nothing.
|
|
278
|
+
|
|
279
|
+
The undo plan used raw truncated json for the same job and cut off mid-path;
|
|
280
|
+
it uses the same summary now, so a step reads
|
|
281
|
+
`would call fs.write_file path …/work/ledger.csv content north,412800`.
|
|
282
|
+
|
|
283
|
+
- **`synartesis close` treated a tidy journal as a usage error.** Nothing left
|
|
284
|
+
open is the ordinary state and the thing somebody runs the command to check.
|
|
285
|
+
It answered with exit 2 and forty lines of unrelated help. It now says
|
|
286
|
+
"nothing is open" and succeeds.
|
|
287
|
+
|
|
288
|
+
Separately, "there is no run to act on" and "no run matches <id>" stopped
|
|
289
|
+
reciting every command. They are facts about the journal rather than about
|
|
290
|
+
what was typed, and the command list buried the one sentence that mattered.
|
|
291
|
+
A mistyped command still gets the full list.
|
|
292
|
+
|
|
293
|
+
- **`-v`, `version` and `help` answer.** `--version` worked and `synartesis
|
|
294
|
+
version` said "unknown command", which is a riddle rather than an answer.
|
|
295
|
+
|
|
296
|
+
- **Listing sessions read every action of every run.** It needs a count and
|
|
297
|
+
three status tallies per run, and it was getting them by materialising every
|
|
298
|
+
row -- snapshots, results and inverses included, which is the bulk of the
|
|
299
|
+
table. So the cost of listing sessions grew with the size of the data those
|
|
300
|
+
sessions had touched rather than with how many there were. It is one grouped
|
|
301
|
+
query now, over a covering index. Measured on forty runs of five hundred
|
|
302
|
+
actions with two-kilobyte snapshots, a hundred-megabyte journal: **76ms to
|
|
303
|
+
2ms**.
|
|
304
|
+
|
|
305
|
+
The index is added the same way as the two in 0.6.12 and for the same reason:
|
|
306
|
+
no row changes, no meaning changes, and an older build opening the same file
|
|
307
|
+
neither notices nor cares.
|
|
308
|
+
|
|
309
|
+
- `synartesis check` now says whether anything is pinned, either way. Silence
|
|
310
|
+
when nothing was would have left the safer state and the unchecked one
|
|
311
|
+
looking identical.
|
|
312
|
+
|
|
5
313
|
## 0.6.12 — 2026-09-13
|
|
6
314
|
|
|
7
315
|
### 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
|