Skip to main content
@multimindlab/reversi-connectors ships fourteen connectors. Every tool has a manifest, and every compensation named in a manifest is itself a tool in the same package that accepts exactly the arguments the argument_map produces. Rollback therefore works end to end, with no glue code. Tool names are <connector>.<tool>, such as fs.write_file or stripe.create_refund.

Run a connector behind the gateway

Print the manifests, then serve the connector over MCP and put the gateway in front of it.
Undo a run with the same package as the compensator:
compensate reads one compensation call as JSON on stdin, runs the matching tool and exits 0 when the undo succeeded. It enables each connector whose credential is in the environment; the filesystem connector is always enabled, rooted at --root. The call’s idempotency_key is passed to Stripe as the Idempotency-Key header, so a retried rollback never repeats a side effect. <name> is one of fs, github, slack, stripe, gitlab, teams, linear, jira, gcalendar, postgres, aws, gcp, azure, kubernetes. Credentials are read from the environment only; a flag such as --token is not accepted. compensate enables every connector whose main credential variable is set (GITHUB_TOKEN, SLACK_BOT_TOKEN, STRIPE_SECRET_KEY, GITLAB_TOKEN, TEAMS_TOKEN, LINEAR_API_KEY, JIRA_API_TOKEN, GOOGLE_CALENDAR_TOKEN, POSTGRES_URL, AWS_ACCESS_KEY_ID, GCP_ACCESS_TOKEN, AZURE_STORAGE_TOKEN, KUBE_API_SERVER). Secrets are redacted from every error message. In code:
A failure that leaves the outcome unknown (a network error or timeout on a call that writes) throws EffectUncertainError, so the step is journaled uncertain and rollback reports it instead of guessing.

Filesystem (fs)

Every path is relative to --root. Absolute paths, .., and symlinks that lead outside the root are rejected. The backup directory (default <root>/.reversi-backups) is reserved and hidden from fs.list_dir. Running a compensation a second time succeeds and changes nothing. fs.remove_dir only removes empty directories.

GitHub (github)

Calls https://api.github.com (override with baseUrl) with a bearer token and API version 2022-11-28. Limits:
  • Issues cannot be deleted through the API, so undoing create_issue closes it. The issue stays visible.
  • github.delete_branch is declared idempotent because it must be callable as a compensation. That also means an agent that deletes a branch itself is not undone by rollback. Leave github:branch:delete out of the agent’s scope; compensations run outside the agent’s scope.
  • Recreating a deleted branch needs its SHA, which the connector does not keep.

Slack (slack)

Calls https://slack.com/api. Slack answers HTTP 200 with { "ok": false, "error": "..." } on failure; the connector treats that as a failure. message_not_found, already_reacted and no_reaction count as success where the goal is already met. Limits:
  • A bot can only delete messages it posted, and the workspace may forbid deletion. Then rollback reports the step as failed.
  • People may have replied or seen the message before it is deleted.
  • If the bot had already reacted with the same emoji, add_reaction succeeds without changing anything, and rollback still removes that reaction.

Stripe (stripe)

Calls https://api.stripe.com with form-encoded bodies. Each write carries an Idempotency-Key: the engine’s key when running a compensation, otherwise a fresh random one (so two deliberate identical calls are not merged). Limits:
  • Refunds move money and cannot be recalled. Rollback lists them as unrecoverable and a human must approve each one.
  • A payment intent that has already succeeded cannot be canceled; rollback fails that step. Create intents unconfirmed and confirm them outside the agent.
  • A deleted Stripe customer cannot be restored, and its saved payment methods are gone. Rollback only deletes customers the run created.

GitLab (gitlab)

Calls https://gitlab.com/api/v4. For a self-hosted instance set GITLAB_BASE_URL (or baseUrl in code) to its root; /api/v4 is added. The token goes in the PRIVATE-TOKEN header. project is a numeric ID or a full path such as group/project. Required token scopes: read_api for get_issue; api for everything else. The user behind the token needs at least the Developer role in the project. Limits:
  • The issue is closed, not deleted (as with GitHub): it stays visible in the project.
  • gitlab.delete_branch is idempotent so it can run as a compensation; an agent that calls it itself is not undone. Keep gitlab:branch:delete out of the agent’s scope. Protected branches cannot be deleted without the Maintainer role.

Microsoft Teams (teams)

Calls Microsoft Graph, https://graph.microsoft.com/v1.0, with a bearer token in TEAMS_TOKEN. Obtain the token yourself (the connector does not run an OAuth flow). Microsoft Graph permissions, as we read the Graph reference (confirm against the current reference for your tenant, since Microsoft changes these): Limits:
  • Posting needs a signed-in user (delegated token). A service principal with only application permissions cannot use teams.post_message.
  • softDelete marks the message deleted; members still see a “message deleted” placeholder in the channel, and the message remains in compliance exports. It is not a purge.
  • People may have read or replied to the message before it is deleted.
  • The Graph endpoints and permissions above are implemented from Microsoft’s documentation and tested against a fake; they have not been run against a live tenant.

Linear (linear)

One GraphQL endpoint, https://api.linear.app/graphql. A personal API key is sent as the whole Authorization value (no Bearer prefix). To use an OAuth token instead, pass it prefixed with Bearer . OAuth scopes: read for get_issue; write (or issues:create and comments:create for the create calls) for the rest. A personal API key has the user’s full access. Limits:
  • An archived issue still exists; it is hidden from boards and can be restored in the Linear UI. Linear sends notifications when an issue is created, and archiving does not recall them.
  • GraphQL errors come back with HTTP 200 or 400; both are treated as failures. “Entity not found” counts as success for the two compensations, since the goal (the thing is gone) is met.

Jira (jira)

Jira Cloud REST API v3 at JIRA_BASE_URL (such as https://yourco.atlassian.net), with basic authentication: your account email and an API token. Descriptions and comments are sent as Atlassian Document Format paragraphs. jira.transition_issue is not reversible by itself. It reads the issue’s current status first and returns previous_status and previous_status_id; rollback calls jira.restore_status, which finds a workflow transition that leads to that status and runs it. A transition can be given as an ID, a transition name, or the name of the status it leads to. Jira project permissions the account needs: Browse Projects, Create Issues, Add Comments, Transition Issues, Delete Issues (for the create_issue compensation), Delete Own Comments (for the add_comment compensation). With OAuth 3LO instead of basic auth the scopes are read:jira-work and write:jira-work. Limits:
  • Deleting an issue cannot be undone and the issue key number is not reused. jira.delete_issue is idempotent only so it can run as a compensation; keep jira:issues:delete out of the agent’s scope. Jira refuses to delete an issue that has subtasks.
  • If the workflow has no transition back to the previous status, jira.restore_status fails and the rollback reports the step as failed. The issue history shows both transitions, and workflow post-functions (assignee changes, resolutions, notifications) run each time.

Google Calendar (gcalendar)

Calls https://www.googleapis.com/calendar/v3 with a bearer token in GOOGLE_CALENDAR_TOKEN. calendar_id defaults to primary. Every write passes sendUpdates=none, so no invitation or change emails go out. gcalendar.update_event reads the event first and returns previous, holding the old value (or null if the field was empty) of exactly the fields it changed. gcalendar.restore_event patches those values back; a null removes the field. Fields: summary, description, location, start, end. OAuth scopes: https://www.googleapis.com/auth/calendar.events.readonly for list_events; https://www.googleapis.com/auth/calendar.events for the rest. Limits:
  • Attendees are not managed by this connector, and nothing is emailed. If you need invitations, do that outside the agent.
  • A deleted event goes to the calendar’s trash for a while but is not restored by rollback of create_event; it is deleted.
  • Someone editing the event between update_event and rollback has those edits to the same fields overwritten by the restore.

Postgres (postgres)

Connects with POSTGRES_URL through the pg package. Every tool takes parameters only:
  • Table, schema and column names must match ^[A-Za-z_][A-Za-z0-9_]{0,62}$ and are always double-quoted.
  • Every value is a bound parameter ($1, $2, …). No value is ever written into SQL text. The tests assert the exact SQL and parameters.
How each works:
  • insert_row runs INSERT INTO "t" ("a","b") VALUES ($1,$2) RETURNING "id" (the key column is id unless key_column says otherwise) and returns the key.
  • update_row runs one transaction: BEGIN, SELECT of the old values of the columns being set with FOR UPDATE, UPDATE, COMMIT. It returns the old values; restore_row sets them back.
  • query takes one SELECT or WITH statement with $n placeholders and values. It runs between BEGIN READ ONLY and ROLLBACK, so a write hidden in a WITH is refused by the server. Statements containing ; are rejected up front because a multi-statement string sent without parameters would run outside the guard. At most 1000 rows come back.
  • Objects and arrays are bound as JSON text (for json and jsonb columns). Binary columns are not supported.
  • A server error carries its SQLSTATE (Postgres error 42501: ...) and the transaction is rolled back. A lost connection after a write was sent is EffectUncertainError.
Database privileges: SELECT for query; INSERT for insert_row; UPDATE for update_row and restore_row; DELETE for delete_row. Use a dedicated role. Limits:
  • READ ONLY stops writes to tables, not every side effect: nextval(), advisory locks and functions from extensions still run. Grant the role only what it needs.
  • Rollback of insert_row deletes the row, but sequences stay advanced (gaps in serial ids), triggers and ON DELETE CASCADE rules run, and rows other transactions linked to it meanwhile are affected.
  • restore_row writes the old values back, so updated_at-style triggers fire again and any change by others to those columns since is overwritten.
  • postgres.delete_row deletes data for good. It is idempotent so it can run as a compensation; keep postgres:rows:delete out of the agent’s scope.

Amazon S3 (aws)

Signs requests with AWS Signature Version 4, implemented here with node:crypto (no AWS SDK). Credentials: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, optional AWS_SESSION_TOKEN. Requests go to https://<bucket>.s3.<region>.amazonaws.com (path-style for bucket names containing dots, or against endpoint for MinIO and similar). Content is text by default, or base64 with encoding: "base64"; objects up to 5 MiB. How the undo works: before put_object overwrites or delete_object deletes an existing key, the connector copies it server-side to .reversi-backups/<uuid> in the same bucket and returns backup_key and existed. restore_object copies the backup back over the key, or deletes the key when there was no previous object (backup_key is null). Without s3:ListBucket S3 answers 403 instead of 404 for a missing key, so grant it. Limits:
  • Backup objects are kept (that is what makes the restore repeatable). Add a lifecycle rule that expires .reversi-backups/.
  • Copies use CopyObject, which is limited to 5 GiB per object; larger objects fail.
  • There is no precondition on S3 writes here, so a writer racing between the backup and the write can have its change restored over. The Cloud Storage and Blob Storage connectors guard this with generation and ETag preconditions.
  • Bucket event notifications, replication and versioning react to every write, copy and delete the connector makes.
  • .reversi-backups/ keys cannot be written or deleted through the tools and are hidden from list_objects.

Google Cloud Storage (gcp)

Calls the Cloud Storage JSON API at https://storage.googleapis.com with a bearer token in GCP_ACCESS_TOKEN. Same shape as S3, with preconditions: the connector reads the object’s generation, copies that generation to .reversi-backups/<uuid> with rewriteTo (looping until done, so large objects work), and then writes or deletes with ifGenerationMatch set to that generation (0 for a new object). If someone changed the object in between, the write is refused with 412 and nothing is overwritten. Required: OAuth scope https://www.googleapis.com/auth/devstorage.read_write (devstorage.read_only for reads only) and the IAM permissions storage.objects.get, storage.objects.list, storage.objects.create, storage.objects.delete. The role roles/storage.objectAdmin covers them. Limits: as for S3, backup objects remain (add a lifecycle rule); object versioning and Pub/Sub notifications react to every write; restore_object overwrites whatever is at the key now.

Azure Blob Storage (azure)

Calls https://<AZURE_STORAGE_ACCOUNT>.blob.core.windows.net (service version 2023-11-03) with a Microsoft Entra bearer token in AZURE_STORAGE_TOKEN (audience https://storage.azure.com/). Same shape as S3, with ETag preconditions: the blob is copied synchronously to .reversi-backups/<uuid> only if its ETag is unchanged (x-ms-source-if-match), then written with If-Match (or If-None-Match: * for a new blob) or deleted with If-Match. Required Azure roles: Storage Blob Data Reader for reads; Storage Blob Data Contributor for writes and deletes. Limits: the synchronous copy (Copy Blob From URL) is limited to 256 MiB; larger blobs fail. Backup blobs remain (add a lifecycle management rule). Soft delete, versioning and Event Grid events on the account react to each operation. Only block blobs are written.

Kubernetes (kubernetes)

Calls the Kubernetes REST API at KUBE_API_SERVER with a bearer token in KUBE_TOKEN. With KUBE_CA_CERT (a path to the cluster CA in PEM) requests go through node:https with that CA; without it the connector uses the runtime’s fetch. Namespaces must be DNS-1123 labels and names DNS-1123 subdomains, so they cannot contain a path or a query.
  • scale_deployment reads the current replica count through the scale subresource, scales with a merge patch, and returns previous_replicas; rollback sets it back.
  • create_configmap fails with 409 if the name exists, so rollback never deletes an object it did not create. Rollback deletes with a UID precondition, so a different ConfigMap that later took the same name is left alone (the 409 is treated as success).
  • rollout_restart stamps the pod template annotation kubectl.kubernetes.io/restartedAt, like kubectl rollout restart. Pods are replaced, so it is declared irreversible and always needs approval. delete_pod likewise.
Limits: a HorizontalPodAutoscaler may scale back on its own; restore_replicas sets the saved count even if someone changed it since; pods that read a ConfigMap you created may have acted on it before it was deleted. The KUBE_CA_CERT transport is tested against a local HTTP server, not a TLS handshake with a real CA.

Certification

Every connector is run through a twelve-check certification suite in npm test. The suite drives each tool against a fake backend with fault injection, using a real Reversi engine for the rollback checks. A connector that fails any check fails the test run. Each check records pass, fail or not_applicable, and not_applicable must carry a reason. Results, generated from the test run (the test suite fails if this table or the reasons below drift from the actual results): Why a check is n/a:
  • fs, Retry: the local filesystem has no network transport that can fail transiently; errors are definite
  • fs, Permission denial: access control is the operating system’s; the connector confines paths to its root instead (covered in filesystem.test.ts)
  • fs, Timeout: local file operations have no network wait to time out
  • fs, Redaction: the connector holds no credential
  • All connectors, Dependency block: no tool manifest declares requires
The dependency block check is implemented and runs as soon as a manifest declares requires; the suite’s self-test proves it with a synthetic connector that does. No shipped connector declares one yet, so for them it has not been exercised. Run reversi-connectors certify all for the checklist, and see packages/connectors/test/certify.test.ts, test/certification.ts and test/fixtures/<name>.ts for the suite. test/certification.selftest.test.ts shows the suite failing a deliberately broken connector (credential leak, lost uncertainty, an undo that does nothing, no timeout).

Manifest drift

connectorSchemaHashes(connector) returns a SHA-256 of each tool’s input schema (canonical JSON, so key order does not matter). Store it as a baseline; detectManifestDrift(manifests, inputSchemas, baseline) later returns the tool names that were added, removed or changed:
The package does not store or compare a baseline for you yet; this is the building block for a gateway or CI step.

Honest limits of the certification

  • Everything is tested against fakes, not live services. Each fake is written from the vendor’s API documentation. If a real service behaves differently from its fake, the suite cannot tell. The first run against a real account is the real test; start with a throwaway project, bucket or database.
  • An HTTP 5xx on a write is reported as an HTTP error naming the status, not as EffectUncertainError. The server answered, so the connector treats the call as failed. Only a lost connection or a timeout is uncertain. For a gateway timeout (502, 504) the write may in fact have been applied; check before retrying.
  • Compensations that leave residue: GitHub and GitLab issues are closed, not deleted; Stripe payment intents are canceled, not removed; Linear issues are archived; Teams messages become soft-deleted placeholders; S3, Cloud Storage and Blob Storage keep .reversi-backups/ copies; the filesystem keeps its backup directory; Postgres sequences stay advanced; a Jira rollback leaves two transitions in the issue history. Where a fake backend does not return to its exact prior state after rollback, the fixture overrides the comparison in code (restored, or a fingerprint that ignores backups), so the difference is stated rather than hidden. Residue the fakes do not model (Jira history, Postgres sequences, notifications) is documented here but not tested.
  • Irreversible tools are certified only for what they can be: Stripe refunds, kubernetes.rollout_restart and kubernetes.delete_pod are checked for forward behaviour, failure, retry, permission, timeout and redaction. They have no compensation, so rollback reports them unrecoverable.
  • Concurrent writers. The restore tools put back what the connector saw. Someone else changing the same thing between the call and the rollback is overwritten, except where a precondition guards it (Cloud Storage generations, Blob Storage ETags, Kubernetes UIDs, Postgres row locks during update_row).