synartesis 0.1.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.
@@ -0,0 +1,77 @@
1
+ # Policy for mcp-server-git, the official git server, run through uvx.
2
+ #
3
+ # Verified against the real server on a real repository.
4
+ #
5
+ # The finding worth carrying away from writing this one: nearly every read this
6
+ # server offers answers in prose meant for a person -- git_status says "On
7
+ # branch main", not {"branch": "main"}. A snapshot is only useful if something
8
+ # can be read back out of it by path, so almost nothing here can be inverted
9
+ # from a captured state, however reversible the underlying git operation is.
10
+ # The limit is the server's, not git's, and pretending otherwise would produce
11
+ # a policy that reports work as undone without having undone it.
12
+ version: 1
13
+
14
+ servers:
15
+ git:
16
+ command: uvx
17
+ args: ["--from", "mcp-server-git", "mcp-server-git"]
18
+
19
+ tools:
20
+ # --- reads -------------------------------------------------------------
21
+ - match: "git.git_status"
22
+ class: readonly
23
+ - match: "git.git_diff"
24
+ class: readonly
25
+ - match: "git.git_diff_staged"
26
+ class: readonly
27
+ - match: "git.git_diff_unstaged"
28
+ class: readonly
29
+ - match: "git.git_log"
30
+ class: readonly
31
+ - match: "git.git_show"
32
+ class: readonly
33
+ - match: "git.git_branch"
34
+ class: readonly
35
+
36
+ # --- the index ---------------------------------------------------------
37
+
38
+ # git_reset here unstages everything, not only what this call staged, so
39
+ # undoing an add can unstage work a person staged by hand beforehand. It is
40
+ # recorded as a compensation for that reason, and undo says so.
41
+ - match: "git.git_add"
42
+ class: compensable
43
+ inverse:
44
+ tool: "git.git_reset"
45
+ args:
46
+ repo_path: "$.repo_path"
47
+
48
+ # Unstaging destroys nothing: every change is still in the working tree, and
49
+ # re-staging is one command. Gating it would spend a person's attention on
50
+ # the cheapest operation here and leave less of it for the commit below.
51
+ - match: "git.git_reset"
52
+ class: irreversible
53
+ gate: never
54
+
55
+ # --- history -----------------------------------------------------------
56
+
57
+ # This server offers no reset, no revert and no way to move a branch, so
58
+ # nothing it exposes can take a commit back. Held for a person.
59
+ - match: "git.git_commit"
60
+ class: irreversible
61
+ gate: always
62
+
63
+ # No delete-branch tool exists here either. `git branch -d` removes one by
64
+ # hand in a second, which is precisely the sentence undo cannot say for you.
65
+ - match: "git.git_create_branch"
66
+ class: irreversible
67
+ gate: always
68
+
69
+ # Checking out changes no history and destroys nothing: git refuses outright
70
+ # when the working tree would be lost. It cannot be undone through this
71
+ # server because no read here reports the current branch as data, so it is
72
+ # recorded honestly as irreversible and deliberately not gated. A gate on
73
+ # every branch switch is a gate people learn to approve without reading, and
74
+ # that costs more than it buys on the commit above.
75
+ - match: "git.git_checkout"
76
+ class: irreversible
77
+ gate: never
@@ -0,0 +1,175 @@
1
+ # Policy for GitHub's MCP server.
2
+ #
3
+ # NOT verified against a live account. Everything here is derived from GitHub's
4
+ # documented API shapes, which is exactly the position the filesystem policy was
5
+ # in before it met a real server and turned out to be wrong in one place. Run
6
+ # `synartesis check --manifest manifests/github.yaml` against your own token
7
+ # before trusting it, and expect to correct at least one field.
8
+ #
9
+ # The npm package @modelcontextprotocol/server-github is deprecated. The
10
+ # supported server is github/github-mcp-server. Synartesis speaks stdio only,
11
+ # so use the local binary rather than GitHub's hosted remote server.
12
+ #
13
+ # The honest limit of this adapter: GitHub has no ordinary-scope delete for most
14
+ # things it creates. An issue can be closed but not un-created; a comment is
15
+ # gone only if the author removes it; a published release stays published.
16
+ # Anything that puts a permanent, publicly visible record into the world is
17
+ # gated rather than dressed up as reversible.
18
+ version: 1
19
+
20
+ servers:
21
+ github:
22
+ command: github-mcp-server
23
+ args: ["stdio"]
24
+ # Read from your shell; never written into this file.
25
+ env:
26
+ GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}"
27
+
28
+ tools:
29
+ # --- reads -------------------------------------------------------------
30
+ - match: "github.get_*"
31
+ class: readonly
32
+ - match: "github.list_*"
33
+ class: readonly
34
+ - match: "github.search_*"
35
+ class: readonly
36
+
37
+ # --- issues ------------------------------------------------------------
38
+
39
+ # An issue cannot be deleted through the API, only closed. Closing is not
40
+ # the same as never having existed: people were notified.
41
+ - match: "github.create_issue"
42
+ class: irreversible
43
+ gate: always
44
+
45
+ # get_issue returns labels and assignees as objects while update_issue takes
46
+ # bare names, so the inverse projects the one field it needs out of each.
47
+ # `[]` reads the same key from every element and does nothing else; it is
48
+ # still a path, not a transform.
49
+ - match: "github.update_issue"
50
+ class: reversible
51
+ snapshot:
52
+ tool: "github.get_issue"
53
+ args:
54
+ owner: "$.owner"
55
+ repo: "$.repo"
56
+ issue_number: "$.issue_number"
57
+ inverse:
58
+ tool: "github.update_issue"
59
+ args:
60
+ owner: "$.owner"
61
+ repo: "$.repo"
62
+ issue_number: "$.issue_number"
63
+ title: "$snapshot.title"
64
+ body: "$snapshot.body"
65
+ state: "$snapshot.state"
66
+ labels: "$snapshot.labels[].name"
67
+ assignees: "$snapshot.assignees[].login"
68
+
69
+ - match: "github.add_issue_comment"
70
+ class: irreversible
71
+ gate: always
72
+
73
+ # --- pull requests -----------------------------------------------------
74
+ - match: "github.create_pull_request"
75
+ class: irreversible
76
+ gate: always
77
+
78
+ - match: "github.update_pull_request"
79
+ class: reversible
80
+ snapshot:
81
+ tool: "github.get_pull_request"
82
+ args:
83
+ owner: "$.owner"
84
+ repo: "$.repo"
85
+ pull_number: "$.pull_number"
86
+ inverse:
87
+ tool: "github.update_pull_request"
88
+ args:
89
+ owner: "$.owner"
90
+ repo: "$.repo"
91
+ pull_number: "$.pull_number"
92
+ title: "$snapshot.title"
93
+ body: "$snapshot.body"
94
+ state: "$snapshot.state"
95
+
96
+ - match: "github.merge_pull_request"
97
+ class: irreversible
98
+ gate: always
99
+ - match: "github.create_pull_request_review"
100
+ class: irreversible
101
+ gate: always
102
+ - match: "github.create_and_submit_pull_request_review"
103
+ class: irreversible
104
+ gate: always
105
+ - match: "github.submit_pending_pull_request_review"
106
+ class: irreversible
107
+ gate: always
108
+
109
+ # A pending review is a draft only its author can see, so deleting one puts
110
+ # nothing into the world. Recreating it restores the draft but not the
111
+ # comments that were inside it, which is a compensation rather than a
112
+ # reversal, and undo will report it as unverified.
113
+ - match: "github.delete_pending_pull_request_review"
114
+ class: compensable
115
+ inverse:
116
+ tool: "github.create_pending_pull_request_review"
117
+ args:
118
+ owner: "$.owner"
119
+ repo: "$.repo"
120
+ pull_number: "$.pull_number"
121
+
122
+ - match: "github.create_pending_pull_request_review"
123
+ class: compensable
124
+ inverse:
125
+ tool: "github.delete_pending_pull_request_review"
126
+ args:
127
+ owner: "$.owner"
128
+ repo: "$.repo"
129
+ pull_number: "$.pull_number"
130
+
131
+ # --- file contents -----------------------------------------------------
132
+
133
+ # The sha must be the blob being replaced at the moment of the revert, which
134
+ # is the one this very call created, not the one it replaced. Passing
135
+ # $snapshot.sha here is the natural mistake and GitHub rejects it as a
136
+ # conflict.
137
+ - match: "github.create_or_update_file"
138
+ class: reversible
139
+ snapshot:
140
+ tool: "github.get_file_contents"
141
+ args:
142
+ owner: "$.owner"
143
+ repo: "$.repo"
144
+ path: "$.path"
145
+ ref: "$.branch"
146
+ inverse:
147
+ tool: "github.create_or_update_file"
148
+ args:
149
+ owner: "$.owner"
150
+ repo: "$.repo"
151
+ path: "$.path"
152
+ branch: "$.branch"
153
+ content: "$snapshot.content"
154
+ sha: "$result.content.sha"
155
+ message: "Revert agent change to $.path"
156
+
157
+ # A multi-file push is one commit touching many paths. This format captures
158
+ # one pre-read per action, not one per path, so there is nothing honest to
159
+ # restore from.
160
+ - match: "github.push_files"
161
+ class: irreversible
162
+ gate: always
163
+
164
+ - match: "github.create_branch"
165
+ class: irreversible
166
+ gate: always
167
+ - match: "github.fork_repository"
168
+ class: irreversible
169
+ gate: always
170
+ - match: "github.create_repository"
171
+ class: irreversible
172
+ gate: always
173
+ - match: "github.create_release"
174
+ class: irreversible
175
+ gate: always
@@ -0,0 +1,96 @@
1
+ # Policy for @modelcontextprotocol/server-memory, the knowledge graph an agent
2
+ # keeps about you between sessions.
3
+ #
4
+ # Verified against the real server: every tool named here was listed by it, and
5
+ # the shapes below were read off its own responses rather than from docs.
6
+ #
7
+ # This is the server the taxonomy fits best. Almost everything it does has an
8
+ # exact opposite sitting next to it in the same toolbox, which is what
9
+ # compensable means. Two things do not, and they are gated rather than
10
+ # approximated.
11
+ version: 1
12
+
13
+ servers:
14
+ memory:
15
+ command: npx
16
+ args: ["-y", "@modelcontextprotocol/server-memory"]
17
+ # Where the graph lives. Point it wherever your client already points it,
18
+ # or the agent will remember into a different file than it reads from.
19
+ env:
20
+ MEMORY_FILE_PATH: "${MEMORY_FILE_PATH}"
21
+
22
+ tools:
23
+ # --- reads -------------------------------------------------------------
24
+ - match: "memory.read_graph"
25
+ class: readonly
26
+ - match: "memory.search_nodes"
27
+ class: readonly
28
+ - match: "memory.open_nodes"
29
+ class: readonly
30
+
31
+ # --- writes ------------------------------------------------------------
32
+
33
+ # The inverse names the result rather than the arguments, and the difference
34
+ # matters: this server silently ignores an entity that already exists, so
35
+ # undoing by argument would delete a Grace who was there long before the
36
+ # agent ran. The result carries only what was actually created.
37
+ #
38
+ # $result.entities, not $result: this server answers with both a text block
39
+ # holding a bare array and a structuredContent object wrapping it, and
40
+ # structured data wins over re-parsed text. Written against the text block,
41
+ # the first version of this policy resolved to nothing and undo halted. It is
42
+ # the same mistake the filesystem policy made, and the reason neither of them
43
+ # can be trusted until it has met a real server.
44
+ #
45
+ # $result is the structured block, not the text one, and here the two
46
+ # disagree: the text is a bare list of entities while structuredContent wraps
47
+ # it as {"entities": [...]}. The structured block is the machine-readable
48
+ # contract, so that is the one a path walks. Writing $result[].name here is
49
+ # the natural mistake, and it was the first thing this policy got wrong
50
+ # against the live server.
51
+ - match: "memory.create_entities"
52
+ class: compensable
53
+ inverse:
54
+ tool: "memory.delete_entities"
55
+ args:
56
+ entityNames: "$result.entities[].name"
57
+
58
+ - match: "memory.create_relations"
59
+ class: compensable
60
+ inverse:
61
+ tool: "memory.delete_relations"
62
+ args:
63
+ relations: "$result.relations"
64
+
65
+ # The one place this policy over-reaches, and it is written down rather than
66
+ # hidden: delete_relations answers in prose, so the inverse has to work from
67
+ # the arguments. Undoing it re-creates every relation the call named,
68
+ # including any that were already absent when it ran.
69
+ - match: "memory.delete_relations"
70
+ class: compensable
71
+ inverse:
72
+ tool: "memory.create_relations"
73
+ args:
74
+ relations: "$.relations"
75
+
76
+ # Deleting an entity also deletes every relation touching it. open_nodes can
77
+ # capture both beforehand, but a policy declares one inverse, and one call
78
+ # cannot put back both the entities and their relations. Restoring half of a
79
+ # graph and reporting it as undone is the exact failure this product exists
80
+ # to prevent, so it asks instead.
81
+ - match: "memory.delete_entities"
82
+ class: irreversible
83
+ gate: always
84
+
85
+ # add_observations and delete_observations are exact opposites that disagree
86
+ # about what to call the same field: one takes `observations[].contents`, the
87
+ # other `deletions[].observations`. A path can read a field and cannot rename
88
+ # one, and the manifest is deliberately not a language, so this inverse
89
+ # cannot be written here at all. That is a limit of the format, stated rather
90
+ # than papered over.
91
+ - match: "memory.add_observations"
92
+ class: irreversible
93
+ gate: always
94
+ - match: "memory.delete_observations"
95
+ class: irreversible
96
+ gate: always
@@ -0,0 +1,66 @@
1
+ # Policy for the toy CRM fixture. Every class in the taxonomy appears here, so
2
+ # this doubles as the worked example of the manifest format.
3
+ version: 1
4
+
5
+ servers:
6
+ crm:
7
+ command: node
8
+ args: ["dist/toy-crm.js"]
9
+
10
+ tools:
11
+ - match: "crm.get_customer"
12
+ class: readonly
13
+
14
+ # The id is assigned by the server, so it is only knowable from the result.
15
+ - match: "crm.create_customer"
16
+ class: compensable
17
+ inverse:
18
+ tool: "crm.delete_customer"
19
+ args:
20
+ id: "$result.id"
21
+
22
+ # A patch, so the inverse has to restore every field rather than re-apply the
23
+ # patch: fields the agent did not touch must still come back unchanged if the
24
+ # same record is updated twice in one run.
25
+ - match: "crm.update_customer"
26
+ class: reversible
27
+ snapshot:
28
+ tool: "crm.get_customer"
29
+ args:
30
+ id: "$.id"
31
+ inverse:
32
+ tool: "crm.update_customer"
33
+ args:
34
+ id: "$.id"
35
+ name: "$snapshot.name"
36
+ email: "$snapshot.email"
37
+ plan: "$snapshot.plan"
38
+ notes: "$snapshot.notes"
39
+
40
+ # Reversible rather than irreversible because the pre-read captures the whole
41
+ # record and restore_customer puts it back under its original id.
42
+ - match: "crm.delete_customer"
43
+ class: reversible
44
+ snapshot:
45
+ tool: "crm.get_customer"
46
+ args:
47
+ id: "$.id"
48
+ inverse:
49
+ tool: "crm.restore_customer"
50
+ args:
51
+ id: "$snapshot.id"
52
+ name: "$snapshot.name"
53
+ email: "$snapshot.email"
54
+ plan: "$snapshot.plan"
55
+ notes: "$snapshot.notes"
56
+
57
+ - match: "crm.restore_customer"
58
+ class: compensable
59
+ inverse:
60
+ tool: "crm.delete_customer"
61
+ args:
62
+ id: "$.id"
63
+
64
+ - match: "crm.send_email"
65
+ class: irreversible
66
+ gate: always
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "synartesis",
3
+ "version": "0.1.0",
4
+ "description": "An undo layer for AI agents.",
5
+ "type": "module",
6
+ "private": false,
7
+ "author": "Arhaan (https://github.com/ArhaanDev24)",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/ArhaanDev24/Synartesis",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ArhaanDev24/Synartesis.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/ArhaanDev24/Synartesis/issues"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "undo",
21
+ "rollback",
22
+ "ai-agents",
23
+ "saga"
24
+ ],
25
+ "bin": {
26
+ "synartesis": "dist/cli.js",
27
+ "synartesis-proxy": "dist/proxy.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "manifests",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint .",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "pretest": "tsup --silent",
45
+ "demo": "tsup --silent && ./demo/filesystem-demo.sh",
46
+ "demo:memory": "tsup --silent && ./demo/memory-demo.sh",
47
+ "prepublishOnly": "tsup"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.30.0",
51
+ "better-sqlite3": "^13.0.3",
52
+ "pino": "^10.3.1",
53
+ "yaml": "^2.9.0",
54
+ "zod": "^4.4.3"
55
+ },
56
+ "devDependencies": {
57
+ "@modelcontextprotocol/server-filesystem": "^2026.7.10",
58
+ "@types/better-sqlite3": "^9.6.0",
59
+ "@types/node": "^26.2.0",
60
+ "eslint": "^10.8.1",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.9.3",
63
+ "typescript-eslint": "^8.67.0",
64
+ "vitest": "^4.1.11"
65
+ },
66
+ "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
67
+ }