patchwork-rb 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/README.md +85 -68
- data/lib/patchwork/version.rb +1 -1
- metadata +12 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02aaad7b12e53f7fd6eb119d03cb576a91cb80afa6d488eecd16dcd251839479
|
|
4
|
+
data.tar.gz: 4743b0e1a4841078b6a85db01048bc9a888a76707e9b746b3098657b7f777785
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 865c4584db5879b87c9e6292ef356ec605e04667c072fa535bb2c715e3cfe1d5ee9e28199c06a71d943bb6a30f4dfc9ec1510c5b89cd080109763ceb4c18e40e
|
|
7
|
+
data.tar.gz: 8711b23744bb966e445103a154a5f827bbdd7a149942b3ffcbe24c25b8358a990c455412a74b88bffe0775af6c97df2ec55aae069637ea27d67cf086253b4617
|
data/README.md
CHANGED
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
The Ruby SDK for [Patchwork](https://usepatchwork.co). It mints session tokens for your users, verifies the tool calls Patchwork makes to your backend, and verifies webhook deliveries.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Documentation](https://docs.usepatchwork.co) · [Changelog](CHANGELOG.md) · [Issues](https://github.com/chromablue-labs/patchwork-rb/issues)
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# Gemfile
|
|
9
9
|
gem "patchwork-rb"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Install `patchwork-rb` and `require "patchwork"`. Do not add `gem "patchwork"` — that name belongs to an unrelated gem.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
It holds no opinion about who your users are. The subject you mint for is an opaque string of your choosing. The gem only interprets it when you ask it to, in a format you declare.
|
|
14
|
+
The subject you mint for is an opaque string you choose. Patchwork stores it, partitions threads and memory on it, and hands it back on every tool call, without ever parsing it. Neither does this gem, unless you declare a format (section 2).
|
|
17
15
|
|
|
18
16
|
## 1. Configure
|
|
19
17
|
|
|
@@ -51,11 +49,11 @@ A subject is the partition key for a user's threads and memory. Patchwork stores
|
|
|
51
49
|
"ws_acme" a whole team, sharing one memory on purpose
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
The
|
|
52
|
+
The [Subjects guide](https://docs.usepatchwork.co/guides/subjects) covers how to choose. Every shape works.
|
|
55
53
|
|
|
56
54
|
**Single-value subjects** need nothing from the gem. Pass the string.
|
|
57
55
|
|
|
58
|
-
**Composite subjects** are
|
|
56
|
+
**Composite subjects** are worth declaring once, so a flipped order or an id containing the delimiter is caught at the call site:
|
|
59
57
|
|
|
60
58
|
```ruby
|
|
61
59
|
# config/initializers/patchwork.rb, next to Patchwork.configure
|
|
@@ -74,7 +72,7 @@ Patchwork::Subject.define(:team_id, :user_id, prefix: "slack") # "slac
|
|
|
74
72
|
Patchwork::Subject.define(:org_id, :user_id, :project_id, delimiter: "|") # "o1|u2|p3"
|
|
75
73
|
```
|
|
76
74
|
|
|
77
|
-
A format
|
|
75
|
+
A format fixes the rules:
|
|
78
76
|
|
|
79
77
|
- **Order comes from the definition.** `encode` is keyword-only, so the order at the call site doesn't matter.
|
|
80
78
|
- **A part containing the delimiter is refused** at encode time. Otherwise it would be split wrongly on decode.
|
|
@@ -91,14 +89,23 @@ If you decode several formats that share a delimiter, try the prefixed ones firs
|
|
|
91
89
|
|
|
92
90
|
## 3. Mint tokens for your users
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
```ruby
|
|
93
|
+
# config/routes.rb
|
|
94
|
+
post "patchwork/mint", to: "patchwork#mint"
|
|
95
|
+
get "patchwork/jwks", to: "patchwork#jwks"
|
|
96
|
+
post "patchwork/webhook", to: "patchwork#webhook"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`mint` has two callers on one URL, and the signature tells them apart. Your frontend asks for a token for the signed-in user; Patchwork asks, server to server, for a token for a subject it already acts for — that covers relay connections and **every Playground run**. Patchwork's call is a signed `POST` carrying `{"subject": "..."}` and expects a bare `{ token, expires_in }` back. The [Session tokens guide](https://docs.usepatchwork.co/guides/session-tokens) has the wire format, and the [Mint endpoint guide](https://docs.usepatchwork.co/guides/mint-endpoint) covers both branches.
|
|
95
100
|
|
|
96
101
|
```ruby
|
|
97
|
-
class
|
|
98
|
-
|
|
102
|
+
class PatchworkController < ApplicationController
|
|
103
|
+
skip_forgery_protection
|
|
104
|
+
|
|
105
|
+
def mint
|
|
106
|
+
return relay_mint if request.headers["Patchwork-Signature"].present?
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
# Every component comes from the server session, never from request params.
|
|
108
|
+
authenticate_user!
|
|
102
109
|
subject = WorkspaceSubject.encode(user_id: current_user.id, workspace_id: current_workspace.id)
|
|
103
110
|
|
|
104
111
|
render json: {
|
|
@@ -106,51 +113,38 @@ class PatchworkTokensController < ApplicationController
|
|
|
106
113
|
expires_in: Patchwork.config.token_ttl
|
|
107
114
|
}
|
|
108
115
|
end
|
|
109
|
-
end
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Pass `connection_id:` when the agent has unpinned customer tools. It takes a connection UUID, not a name.
|
|
113
|
-
|
|
114
|
-
### Patchwork minting for you (relay)
|
|
115
|
-
|
|
116
|
-
Patchwork also calls your mint endpoint, server to server, when it needs a token for a subject it already acts for. That covers relay connections and **every Playground run**. It sends a signed `POST` with `{"subject": "..."}` and expects a bare `{ token, expires_in }` back. Both callers share your one `mint_url`, and the signature is what tells them apart.
|
|
117
|
-
|
|
118
|
-
Give the gateway your mint path and it answers Patchwork's call itself. Your frontend's unsigned request to the same path still reaches your controller above:
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
resolve: ...
|
|
124
|
-
```
|
|
117
|
+
def jwks
|
|
118
|
+
render json: Patchwork::SigningKey.jwks
|
|
119
|
+
end
|
|
125
120
|
|
|
126
|
-
|
|
121
|
+
private
|
|
127
122
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
def relay_mint
|
|
124
|
+
render json: Patchwork::Mint.relay(
|
|
125
|
+
body: request.raw_post,
|
|
126
|
+
signature: request.headers["Patchwork-Signature"],
|
|
127
|
+
path: request.path
|
|
128
|
+
), status: :created
|
|
129
|
+
rescue Patchwork::InvalidSignature
|
|
130
|
+
head :unauthorized
|
|
131
|
+
rescue Patchwork::Mint::BadRequest
|
|
132
|
+
head :bad_request
|
|
134
133
|
end
|
|
135
|
-
# ...the direct branch above
|
|
136
134
|
end
|
|
137
135
|
```
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
Build the subject from server state, never from request params. On the direct branch that is the whole of your authorisation.
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
`Mint.relay` mints for exactly the subject Patchwork sent, never a default, and verifies the signature before it reads the subject at all. Pass `connection_id:` to `issue` when the agent has unpinned customer tools; it takes a connection UUID, not a name.
|
|
142
140
|
|
|
143
|
-
|
|
141
|
+
`jwks` publishes the public half of your signing key, so Patchwork can verify what you minted. Set its URL on your workspace.
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
get "/patchwork/jwks", to: ->(_env) {
|
|
147
|
-
[ 200, { "content-type" => "application/json" }, [ Patchwork::SigningKey.jwks.to_json ] ]
|
|
148
|
-
}
|
|
149
|
-
```
|
|
143
|
+
If you mount the gateway in section 4, `mint_path: "/patchwork/mint"` makes it answer the relay call before your controller sees it, and you can drop `relay_mint`. Your frontend's unsigned request still reaches `mint`.
|
|
150
144
|
|
|
151
145
|
## 4. Verify the tool calls Patchwork makes to you
|
|
152
146
|
|
|
153
|
-
When an agent calls one of your tools, Patchwork sends the request with a `Patchwork-Signature` header and the session token you minted. `Patchwork::Gateway` is Rack middleware that verifies both. It then hands the token's subject to a `resolve` callable, which you write, to decide what the request acts as.
|
|
147
|
+
When an agent calls one of your tools, Patchwork sends the request with a `Patchwork-Signature` header and the session token you minted. `Patchwork::Gateway` is Rack middleware that verifies both. It then hands the token's subject to a `resolve` callable, which you write, to decide what the request acts as. The [Tool calls guide](https://docs.usepatchwork.co/guides/tool-calls) describes what Patchwork sends.
|
|
154
148
|
|
|
155
149
|
```ruby
|
|
156
150
|
# config/initializers/patchwork.rb
|
|
@@ -236,7 +230,7 @@ Failure behaviour:
|
|
|
236
230
|
|
|
237
231
|
### What the signature does and doesn't cover
|
|
238
232
|
|
|
239
|
-
The signature covers the method, the path and the raw body. **It does not cover the query string**, and Patchwork sends a GET tool's arguments as query parameters. So a GET tool's arguments are not integrity-protected. Anyone who can alter a request in flight (a TLS-terminating proxy, for example) can change them without breaking the signature. Give any tool whose arguments matter, such as ids, amounts or anything that writes, a `POST` binding, where the arguments travel in the signed body.
|
|
233
|
+
The signature covers the method, the path and the raw body, as specified in the [Signing guide](https://docs.usepatchwork.co/guides/signing). **It does not cover the query string**, and Patchwork sends a GET tool's arguments as query parameters. So a GET tool's arguments are not integrity-protected. Anyone who can alter a request in flight (a TLS-terminating proxy, for example) can change them without breaking the signature. Give any tool whose arguments matter, such as ids, amounts or anything that writes, a `POST` binding, where the arguments travel in the signed body.
|
|
240
234
|
|
|
241
235
|
The signed path is the one your app sees. If a proxy rewrites paths before your app does, mount the gateway where the original path is still intact.
|
|
242
236
|
|
|
@@ -256,31 +250,43 @@ Use a cache that every app process shares. A per-process cache only stops replay
|
|
|
256
250
|
|
|
257
251
|
## 5. Health check
|
|
258
252
|
|
|
259
|
-
The gateway answers `POST /patchwork/up` (also when it's mounted under a prefix) with a proof that only the shared secret can produce. That's how **Test** on a connection in the Patchwork console reports **verified** rather than just reachable.
|
|
253
|
+
The gateway answers `POST /patchwork/up` (also when it's mounted under a prefix) with a proof that only the shared secret can produce. That's how **Test** on a connection in the Patchwork console reports **verified** rather than just reachable.
|
|
254
|
+
|
|
255
|
+
Without the gateway, it is another action on the same controller. Route `post "patchwork/up", to: "patchwork#up"`:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
def up
|
|
259
|
+
render json: Patchwork::HealthCheck.respond(body: request.raw_post)
|
|
260
|
+
rescue Patchwork::HealthCheck::BadRequest
|
|
261
|
+
head :bad_request
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Pass `health_check: false` if you want to own the route while still using the gateway for tool calls.
|
|
260
266
|
|
|
261
267
|
## 6. Verify webhooks
|
|
262
268
|
|
|
263
|
-
Webhooks use the same signing scheme, keyed with the **webhook endpoint's own secret**, not the request secret.
|
|
269
|
+
Webhooks use the same signing scheme, keyed with the **webhook endpoint's own secret**, not the request secret. The [Events guide](https://docs.usepatchwork.co/guides/events) lists the event types.
|
|
264
270
|
|
|
265
|
-
|
|
266
|
-
class PatchworkWebhooksController < ActionController::API
|
|
267
|
-
def create
|
|
268
|
-
event = Patchwork::Webhook.verify!(
|
|
269
|
-
body: request.raw_post,
|
|
270
|
-
signature: request.headers["Patchwork-Signature"],
|
|
271
|
-
secret: ENV.fetch("PATCHWORK_WEBHOOK_SECRET"),
|
|
272
|
-
path: "/hooks/patchwork"
|
|
273
|
-
)
|
|
271
|
+
Deliveries land on the same controller:
|
|
274
272
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
```ruby
|
|
274
|
+
def webhook
|
|
275
|
+
event = Patchwork::Webhook.verify!(
|
|
276
|
+
body: request.raw_post,
|
|
277
|
+
signature: request.headers["Patchwork-Signature"],
|
|
278
|
+
secret: ENV.fetch("PATCHWORK_WEBHOOK_SECRET"),
|
|
279
|
+
path: "/patchwork/webhook"
|
|
280
|
+
)
|
|
279
281
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
case event.type
|
|
283
|
+
when "run.completed" then FulfilJob.perform_later(event.run_id)
|
|
284
|
+
when "run.failed" then alert(event.data)
|
|
283
285
|
end
|
|
286
|
+
|
|
287
|
+
head :ok
|
|
288
|
+
rescue Patchwork::InvalidSignature
|
|
289
|
+
head :unauthorized
|
|
284
290
|
end
|
|
285
291
|
```
|
|
286
292
|
|
|
@@ -299,21 +305,28 @@ end
|
|
|
299
305
|
|
|
300
306
|
## MCP login bridge
|
|
301
307
|
|
|
302
|
-
If you export an agent over MCP, Patchwork sends your user to a login bridge you host, and the bridge hands back a signed subject
|
|
308
|
+
If you export an agent over MCP, Patchwork sends your user to a login bridge you host, and the bridge hands back a signed subject. It is signed with the same key you mint with:
|
|
309
|
+
|
|
310
|
+
Route `get "patchwork/bridge", to: "patchwork#bridge"` and add the action alongside the others:
|
|
303
311
|
|
|
304
312
|
```ruby
|
|
305
|
-
|
|
313
|
+
def bridge
|
|
314
|
+
authenticate_user!
|
|
306
315
|
subject = WorkspaceSubject.encode(user_id: current_user.id, workspace_id: chosen_workspace.id)
|
|
307
316
|
assertion = Patchwork::BridgeAssertion.issue(
|
|
308
317
|
subject: subject,
|
|
309
318
|
nonce: params[:state],
|
|
310
319
|
audience: PATCHWORK_CALLBACK
|
|
311
320
|
)
|
|
312
|
-
|
|
321
|
+
|
|
322
|
+
redirect_to "#{PATCHWORK_CALLBACK}?state=#{params[:state]}&assertion=#{assertion}",
|
|
323
|
+
allow_other_host: true
|
|
313
324
|
end
|
|
314
325
|
```
|
|
315
326
|
|
|
316
|
-
|
|
327
|
+
`allow_other_host:` is required — the redirect leaves your domain.
|
|
328
|
+
|
|
329
|
+
Authenticate the user first, and bake the workspace choice into the subject. The assertion is bound to the `state` nonce and the callback audience, and lives 120 seconds at most. That ceiling is enforced at the callback, so asking for longer raises.
|
|
317
330
|
|
|
318
331
|
## Errors
|
|
319
332
|
|
|
@@ -343,6 +356,10 @@ Patchwork::SessionToken.verify(token) # => claims, or raises Patchwork::Invali
|
|
|
343
356
|
|
|
344
357
|
Ruby 3.1+. One runtime dependency: `jwt` (>= 2.10.3, < 4, excluding 3.0.0–3.1.2, which are affected by GHSA-c32j-vqhx-rx3x). `Patchwork::Gateway` also needs `rack`, and is only defined when Rack is loaded. It works with any Rack app: Rails, Sinatra, Hanami or bare Rack. `Patchwork::Rails::Presented` needs `activesupport` and loads only when you require `patchwork/rails`.
|
|
345
358
|
|
|
359
|
+
## Support
|
|
360
|
+
|
|
361
|
+
Bugs and questions: [github.com/chromablue-labs/patchwork-rb/issues](https://github.com/chromablue-labs/patchwork-rb/issues). The platform documentation is at [docs.usepatchwork.co](https://docs.usepatchwork.co).
|
|
362
|
+
|
|
346
363
|
## Licence
|
|
347
364
|
|
|
348
365
|
MIT.
|
data/lib/patchwork/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patchwork-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chromablue Labs
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: jwt
|
|
@@ -55,9 +56,8 @@ dependencies:
|
|
|
55
56
|
version: 3.1.2
|
|
56
57
|
description: 'Server-side helpers for integrating with Patchwork: RS256 session tokens,
|
|
57
58
|
HMAC request signing and verification, webhook signature verification, and Rack
|
|
58
|
-
middleware for
|
|
59
|
-
|
|
60
|
-
arrives in 0.2.'
|
|
59
|
+
middleware for the tool calls Patchwork makes to your backend.'
|
|
60
|
+
email:
|
|
61
61
|
executables: []
|
|
62
62
|
extensions: []
|
|
63
63
|
extra_rdoc_files: []
|
|
@@ -86,9 +86,12 @@ licenses:
|
|
|
86
86
|
- MIT
|
|
87
87
|
metadata:
|
|
88
88
|
homepage_uri: https://usepatchwork.co
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
documentation_uri: https://github.com/chromablue-labs/patchwork-rb#readme
|
|
90
|
+
source_code_uri: https://github.com/chromablue-labs/patchwork-rb
|
|
91
|
+
changelog_uri: https://github.com/chromablue-labs/patchwork-rb/blob/main/CHANGELOG.md
|
|
92
|
+
bug_tracker_uri: https://github.com/chromablue-labs/patchwork-rb/issues
|
|
91
93
|
rubygems_mfa_required: 'true'
|
|
94
|
+
post_install_message:
|
|
92
95
|
rdoc_options: []
|
|
93
96
|
require_paths:
|
|
94
97
|
- lib
|
|
@@ -103,7 +106,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
106
|
- !ruby/object:Gem::Version
|
|
104
107
|
version: '0'
|
|
105
108
|
requirements: []
|
|
106
|
-
rubygems_version: 3.
|
|
109
|
+
rubygems_version: 3.5.22
|
|
110
|
+
signing_key:
|
|
107
111
|
specification_version: 4
|
|
108
112
|
summary: Ruby SDK for Patchwork — mint session tokens, verify tool calls and webhooks
|
|
109
113
|
test_files: []
|