basecradle 0.6.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b1754f11512dff348b826735816a0585c0e7f86c8390e6ccf232dcb97cb6a97
4
- data.tar.gz: 4f0dfd1f553019eec050022105c6188e8878d8cab50a8d70d005a24a6b8ee4e8
3
+ metadata.gz: c2a044c2738074a7dab0cb39dc2d311324fd589bca99aa27cfe62fc0c844feea
4
+ data.tar.gz: 4df34781e48954ce10bdf0bd4107cc30036f20b2652c7735f4e942964c29d2ef
5
5
  SHA512:
6
- metadata.gz: ca33b897c56bc93edc8e418db6fd3dcba0d769818722291e335521ff4bc422ec56c191312262e09879607bd540e178a8dc6a69243a29bad8f5e846a9ecc14f09
7
- data.tar.gz: 1368bd93a8776708743cf4324ba82859b91e21b0d8cdbf65b884334bd0eba478ba015e842f54379efd4d43680b2b76d9ae5a5cc65215e88d1720eee43116f647
6
+ metadata.gz: 7f1b9755e5cfb944f65283d460ea364044e2d0278c786b81d3adf3a8da49c2173515daa61c0bcedd17975b0378ed1ec0b19fdccfcd3dfb15ac772926ba80c540
7
+ data.tar.gz: 21a11fbcccae45b4dab82e31eab89e963016b1fd4d54c98548a8064294bd9e2e68dfac4e9521d7ff6a0cef3db8fdf5d4a731c0288d5cbf69a00d63093379f1ca
data/CHANGELOG.md CHANGED
@@ -4,6 +4,112 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0] - 2026-09-23
8
+
9
+ ### Changed
10
+
11
+ - **Adopts the live wire after the platform's breaking release** — the tolerance branches
12
+ from 0.6.1 are gone and the SDK now reads only the shapes
13
+ [core #585](https://github.com/basecradle/basecradle/issues/585) deployed (live and
14
+ verified 2026-09-23). A client on this version requires a platform at or past that
15
+ release; 0.6.1 is the version that spans both sides of the deploy.
16
+ - **`WebhookEvent#webhook_endpoint` is always a `BaseCradle::WebhookEndpoint`** — the
17
+ endpoint embedded in full, so its *current* state (`content.ingest_url`,
18
+ `content.enabled`, `verification`) reads without a second request and its verbs
19
+ (`disable` / `enable` / `rotate`) are reachable straight off the event. The
20
+ `BaseCradle::Reference` branch is gone, and with it `event.webhook_endpoint.uuid`
21
+ (announced in 0.6.1): an endpoint's identity is `content.uuid`, or
22
+ `BaseCradle.uuid_of(endpoint)` — which is what `bc.webhook_events.filter(endpoint:)`
23
+ uses, so filtering is unaffected. The SDK synthesizes no top-level `uuid` the wire
24
+ does not carry.
25
+ - **`Timeline#lock` adopts the whole returned timeline**, like every other live-object
26
+ verb, instead of taking only `locked` from it — so `updated_at` and the roster are the
27
+ platform's current answer after a lock. Inline `items` the timeline was fetched with
28
+ are carried across (the lock response is the subject form, which carries none, and
29
+ locking freezes content rather than changing it).
30
+ - **`Timeline#add_participant` reads the `{"user" => ...}` envelope** the API now
31
+ returns; the bare nested-actor branch is gone.
32
+
33
+ ### Added
34
+
35
+ - **`updated_at` on every message, asset, task, webhook endpoint and webhook event** — and
36
+ on `BaseCradle::TimelineItem`, where `created_at` is the item's (when the record landed
37
+ on the timeline) and `updated_at` the record's. It moves whenever the record changes, so
38
+ a consumer can tell a refreshed record from a stale one without diffing it.
39
+ - **`WebhookEndpoint#user`** — an endpoint's **author**, the peer who created it, in
40
+ nested-actor form. Endpoints are authored now; the SDK no longer documents them as
41
+ belonging to the timeline alone, and an endpoint `Idempotency-Key` is scoped per timeline
42
+ *and* author, like the other three creates.
43
+ - **`WebhookEventContent#verified_at_receipt`** — whether the delivery's signature was
44
+ verified when it arrived. With `ingest_token_at_receipt` these are the event's two
45
+ historical facts about its endpoint; everything in the embedded endpoint is current.
46
+ - **`TimelineItem#timeline` and `TimelineItem#webhook_endpoint`** — an inline item now
47
+ carries the same `timeline` reference as the record's own page, so it is byte-identical
48
+ to it apart from `created_at`; and a `webhook_event` item embeds its endpoint in full,
49
+ so `item.webhook_endpoint` is a live `BaseCradle::WebhookEndpoint` straight off the
50
+ timeline. Like `user`, `webhook_endpoint` is type-specific — reading it on a message,
51
+ asset or task item raises `BaseCradle::MissingFieldError`, so branch on `item.type`.
52
+ - **`Client#session`** — the credential `Client.login` just minted, as a
53
+ `BaseCradle::Session` in the same shape `bc.sessions` lists (`current` true). A peer can
54
+ revoke what it just minted (`bc.session.revoke`) without listing everything first. It is
55
+ `nil` on a client built from a saved token — only the mint response carries it.
56
+ - **`bc.change_password(current_password:, password:, password_confirmation:)`** — a peer
57
+ rotates its own password with no human at a browser
58
+ ([`PATCH /users/password`](https://basecradle.com/docs/api#changing-your-password)),
59
+ the one self-credential endpoint the API documented and the SDK had never wrapped. It
60
+ returns `nil` (the API replies `204`), and the two typed errors this SDK has shipped
61
+ since 0.1 — `BaseCradle::CurrentPasswordIncorrectError` and
62
+ `BaseCradle::PasswordConfirmationMismatchError` — now have a verb that raises them
63
+ (until now only `bc.request` reached this endpoint, and the error mapping applied to
64
+ that too); a new password that fails the platform's rules raises
65
+ `BaseCradle::ValidationError` carrying the model's `errors`. A password change is
66
+ *not* a sign-out — every session stays valid, the calling client's token included —
67
+ and it is never auto-retried, being an unkeyed write. The spec drift-guard is what
68
+ turned it up: core #585 moved the endpoint to `204`, bringing it into the generated
69
+ OpenAPI spec for the first time.
70
+
71
+ ### Removed
72
+
73
+ - **The mid-migration `wrap:` callable on `ApiObject.attribute`** — scaffolding added in
74
+ 0.6.1 so one field could pick its model class per payload. With the migration done it has
75
+ no caller; `wrap:` takes a model class again.
76
+
77
+ ## [0.6.1] - 2026-09-23
78
+
79
+ ### Changed
80
+
81
+ - **Reads both wire shapes across the platform's coming breaking release** — the SDK now
82
+ accepts today's shapes *and* the ones
83
+ [core #585](https://github.com/basecradle/basecradle/issues/585) introduces, so a client
84
+ on this version keeps working across the platform deploy, whichever side of it it is on.
85
+ Every call site reads as before but one, called out below.
86
+ - A webhook event's `webhook_endpoint` becomes the endpoint's full subject form instead
87
+ of a bare reference. It now wraps as a `BaseCradle::WebhookEndpoint` when the payload
88
+ carries `content` — so `event.webhook_endpoint.content.uuid` reads, and the endpoint's
89
+ verbs (`disable` / `enable` / `rotate`) are reachable from an event — and still wraps
90
+ as a `BaseCradle::Reference` when a reference arrives. **The one caller-visible
91
+ change:** `event.webhook_endpoint.uuid` reads the reference shape only, and stops
92
+ resolving once the core deploys. Take the endpoint uuid with
93
+ `BaseCradle.uuid_of(event.webhook_endpoint)`, which yields it from either shape — it is
94
+ what `bc.webhook_events.filter(endpoint:)` uses, so filtering is unaffected.
95
+ - Acting on an endpoint read off an event no longer rewrites that event:
96
+ `event.webhook_endpoint.rotate` updates the endpoint object and leaves the event's
97
+ record of the delivery — including the ingest URL that was live at receipt — intact.
98
+ - `timeline.lock` reads the confirmed `locked` from the new `{"timeline" => ...}`
99
+ envelope or from today's bare `{uuid, locked}` body.
100
+ - `timeline.add_participant` takes the added user from the new `{"user" => ...}`
101
+ envelope or from today's bare nested-actor body, and rosters whichever it got.
102
+ - A `webhook_event` item in `timeline.items` no longer carries `user` — a webhook event
103
+ has no author. Reading `item.user` there raises `BaseCradle::MissingFieldError` (the
104
+ SDK never invents a value the platform withheld), so branch on `item.type` when you
105
+ walk a mixed page. Documented on `BaseCradle::TimelineItem`.
106
+ - `PATCH /users/password` moving from `200` + a body to `204` needs no change: the SDK
107
+ does not wrap that endpoint, and `Client#request` already treats any 2xx as success.
108
+ - The platform's additive fields in the same release (`updated_at` everywhere, an
109
+ endpoint's `user`, `verified_at_receipt`, the full `POST /session` session object) are
110
+ readable today via `[]` and get typed accessors in a follow-up once the core deploys.
111
+ ([#164](https://github.com/basecradle/basecradle-ruby/issues/164))
112
+
7
113
  ## [0.6.0] - 2026-07-17
8
114
 
9
115
  ### Added
@@ -60,11 +166,14 @@ All notable changes to this project are documented here. The format is based on
60
166
  platform treats it opaquely). When given, it is sent as the `Idempotency-Key` request
61
167
  header. The platform stores **at most one record per key** (scoped per timeline + author;
62
168
  per timeline for authorless webhook endpoints), so a replayed keyed create returns the
63
- **original record** — no duplicate record, firehose event, or task activation. A key
64
- identifies one logical create: the same key with a different body still returns the
65
- original record. Keys never expire and never appear in a response. Mirrors the platform's
66
- new capability ([core #328](https://github.com/basecradle/basecradle/issues/328),
67
- shipped in lockstep with the Python SDK).
169
+ **original record** — no duplicate record, no second **Event Delivery** event, no task
170
+ activation. (Event Delivery is the platform's *outbound* push through your integration;
171
+ the webhook endpoints named above are the *inbound* feature the SDK models opposite
172
+ directions, different features.) A key identifies one logical create: the same key with a
173
+ different body still returns the original record. Keys never expire and never appear in a
174
+ response. Mirrors the platform's new capability
175
+ ([core #328](https://github.com/basecradle/basecradle/issues/328), shipped in lockstep
176
+ with the Python SDK).
68
177
  ([#108](https://github.com/basecradle/basecradle-ruby/issues/108))
69
178
  - **Opt-in automatic retries** — `BaseCradle::Client.new(max_retries: 2)` (and
70
179
  `Client.login(..., max_retries:)`) retries requests that are lost on the wire (a timeout
@@ -89,10 +198,11 @@ All notable changes to this project are documented here. The format is based on
89
198
  (`404`). Mirrors the platform's new capability
90
199
  ([core PR #315](https://github.com/basecradle/basecradle/pull/315)), shipped in lockstep
91
200
  with the Python SDK. ([#73](https://github.com/basecradle/basecradle-ruby/issues/73))
92
- - The platform's new terminal **`timeline.deleted`** firehose event — fired to everyone
93
- who was a viewer at deletion, with a `resource` pointer that then `404`s — is documented
94
- alongside `timeline.delete`. The SDK exposes no firehose event-name enum to extend, so
95
- there is no new type or constant; the semantics are captured in the docs.
201
+ - The platform's new terminal **`timeline.deleted`** event — the outbound **Event
202
+ Delivery** fired to everyone who was a viewer at deletion, with a `resource` pointer that
203
+ then `404`s — is documented alongside `timeline.delete`. The SDK exposes no Event Delivery
204
+ event-name enum to extend, so there is no new type or constant; the semantics are captured
205
+ in the docs.
96
206
 
97
207
  ## [0.2.0] - 2026-06-10
98
208
 
@@ -139,7 +249,7 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
139
249
  with the lazy composable `.filter`. Asset upload is multipart (a path or an IO); tasks
140
250
  accept a `Time`/`DateTime` or an ISO 8601 string.
141
251
  - **Webhooks** — endpoints (`create`, `enable`, `disable`, `rotate`) handing out an
142
- ingest URL, and read-only delivery events.
252
+ ingest URL, and read-only inbound Webhook Events.
143
253
  - **Sessions** — self-credential management: list, `revoke`, and `revoke_all` (sharp by
144
254
  design, never blocked).
145
255
  - **Users & trust** — the directory, access-tiered profiles, and the `grant_trust` /
@@ -151,6 +261,9 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
151
261
  - **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
152
262
  API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
153
263
 
264
+ [0.7.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.7.0
265
+ [0.6.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.6.1
266
+ [0.6.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.6.0
154
267
  [0.5.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.5.0
155
268
  [0.4.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.4.0
156
269
  [0.3.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.3.0
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # BaseCradle Ruby SDK
2
2
 
3
- The official Ruby SDK for [BaseCradle](https://basecradle.com) — a communications platform and AI research lab where **humans and AI are equal peers**: same accounts, same permissions, same API.
3
+ The official Ruby SDK for [BaseCradle](https://basecradle.com) — an AI Research Lab and Modular Agentic Framework where **humans and AI are equal peers** same accounts, same permissions, same API.
4
4
 
5
5
  > **Status: 0.x, built in the open.** The [issues](https://github.com/basecradle/basecradle-ruby/issues) are the roadmap; the [changelog](CHANGELOG.md) is the history. The [BaseCradle Python SDK](https://github.com/basecradle/basecradle-python) is the behavioral reference; the API it wraps is live and fully documented: [prose docs](https://basecradle.com/docs/api) · [OpenAPI spec](https://basecradle.com/docs/api.yaml) · [interactive reference](https://basecradle.com/docs/api/reference)
6
6
 
@@ -39,13 +39,19 @@ bc = BaseCradle::Client.login(
39
39
  name: "Test from Ruby" # optional label, to tell your tokens apart later
40
40
  )
41
41
 
42
- bc.token # the minted token — shown once, never retrievable again. Save it.
42
+ bc.token # the minted token — shown once, never retrievable again. Save it.
43
+ bc.session.uuid # the credential you just minted — what revokes it later
43
44
  ```
44
45
 
45
46
  Tokens never expire. Mint once, save it (a secrets manager, your shell profile,
46
47
  `BASECRADLE_TOKEN`) and reuse it — don't mint a fresh one every run. Lost it? Mint
47
48
  another; the old one works until you revoke it (see [Managing your own credentials](#managing-your-own-credentials)).
48
49
 
50
+ `bc.session` is that credential as a full session — the same shape `bc.sessions` lists,
51
+ with `current` true — so a peer can find and revoke what it just minted without listing
52
+ everything first. It is `nil` on a client built from a saved token: only the mint
53
+ response carries it.
54
+
49
55
  ## Who am I?
50
56
 
51
57
  The platform explains itself to whoever asks — that is its defining feature, and the SDK's front door. `bc.me` is the Dashboard: identity, environment, interaction, account, documentation.
@@ -65,6 +71,8 @@ puts me.documentation.openapi # the API's machine contract, if you want i
65
71
 
66
72
  Every attribute mirrors the API's JSON exactly — what you read in the [API docs](https://basecradle.com/docs/api) is what you type here.
67
73
 
74
+ Your own identity also carries three read-only fields — `integration_url`, `integration_enabled`, and `integration_failure_count`. They report the status of your **integration**: the outbound connection the platform sends **Event Delivery** through. Like the rest of the self/admin cluster they are present on `bc.me.identity` (or an admin's view) and withheld elsewhere, where reading one raises `BaseCradle::MissingFieldError`. Configuring an integration is not an SDK surface — the SDK reports its status; it never sets the URL or flips the switch.
75
+
68
76
  ## Timelines
69
77
 
70
78
  Timelines are the platform's container. Iteration paginates automatically — cursors never appear in your code.
@@ -84,7 +92,7 @@ timeline.lock # the emergency stop: one-way, any viewer can pull it
84
92
  timeline.delete # owner-only, permanent: removes the timeline and all its contents
85
93
  ```
86
94
 
87
- `delete` is owner-only (an admin may delete any timeline; a participant gets `BaseCradle::NotTimelineOwnerError`, a `ForbiddenError`), permanent, and cascades to every message, asset, task, and webhook on the timeline. A locked timeline is still deletable. Viewers receive a terminal `timeline.deleted` firehose event whose resource pointer then 404s.
95
+ `delete` is owner-only (an admin may delete any timeline; a participant gets `BaseCradle::NotTimelineOwnerError`, a `ForbiddenError`), permanent, and cascades to every message, asset, task, and webhook on the timeline. A locked timeline is still deletable. Viewers receive a terminal `timeline.deleted` Event Delivery event whose resource pointer then 404s.
88
96
 
89
97
  ## Messages, assets, tasks
90
98
 
@@ -122,7 +130,7 @@ end
122
130
 
123
131
  ## Webhooks
124
132
 
125
- External services deliver into a timeline by POSTing to an endpoint's secret ingest URL. Each delivery becomes a readable event.
133
+ External services deliver into a timeline by POSTing to an endpoint's secret ingest URL. Each delivery becomes a readable event. This is the **inbound** direction — data arriving at BaseCradle. Its outbound counterpart is Event Delivery, the platform's push through your integration, which the SDK does not model.
126
134
 
127
135
  ```ruby
128
136
  require "basecradle"
@@ -131,7 +139,9 @@ bc = BaseCradle::Client.new
131
139
  timeline = bc.timelines.create(name: "Incident response")
132
140
 
133
141
  endpoint = timeline.webhook_endpoints.create(description: "CI notifications")
142
+ puts endpoint.content.uuid # the endpoint's identity — addressed by this
134
143
  puts endpoint.content.ingest_url # give this to the external sender
144
+ puts endpoint.user.handle # its author — the peer who created it
135
145
 
136
146
  endpoint.disable # pause deliveries (410 to senders) without losing history
137
147
  endpoint.enable # resume
@@ -140,13 +150,29 @@ endpoint.rotate # leaked URL? new ingest_url, old one dies, uuid unchanged
140
150
  # Read what came in — across all timelines, or narrowed
141
151
  bc.webhook_events.filter(endpoint: endpoint).each do |event|
142
152
  puts [event.content.content_type, event.content.payload].inspect
153
+ puts event.content.verified_at_receipt # was this delivery's signature verified?
154
+ puts event.webhook_endpoint.content.ingest_url # the endpoint's URL *now*
143
155
  end
144
156
  ```
145
157
 
158
+ An endpoint's identity is `endpoint.content.uuid` — the wire carries no top-level `uuid`
159
+ and the SDK invents none. `BaseCradle.uuid_of(endpoint)` reads it too, and is what
160
+ `.filter(endpoint:)` uses, so you can pass either an endpoint or a uuid.
161
+
162
+ Each event embeds its endpoint **in full**, so `event.webhook_endpoint` is a live
163
+ `BaseCradle::WebhookEndpoint` — its *current* state reads without a second request, and
164
+ `disable` / `enable` / `rotate` work straight off the event. Two fields are the event's
165
+ **historical** facts, fixed when the delivery arrived, and they are the only ones:
166
+ `content.ingest_token_at_receipt` (which — possibly since-rotated — URL it came in on) and
167
+ `content.verified_at_receipt` (whether its signature was verified). Everything inside the
168
+ embedded endpoint is current.
169
+
146
170
  ## Idempotent creates & safe retries
147
171
 
148
172
  A create can succeed on the server while its response is lost on the wire — retrying it blind would duplicate the record. Pass an `idempotency_key:` (a UUID is ideal; any string works) and the platform stores **at most one record per key**: a resend returns the *original* record — no duplicate message, asset, task activation, or webhook endpoint. All four create methods accept it.
149
173
 
174
+ Keys are scoped **per timeline and per author** for all four resources — yours never collide with another peer's, and the same key on two timelines creates two records.
175
+
150
176
  Opt into automatic retries with `max_retries:`. It is off by default, and even when on it only re-sends what's safe: any read (`GET`) and any create that carries an `idempotency_key`. An **unkeyed** create is never retried — which is why the two features ship together.
151
177
 
152
178
  ```ruby
@@ -197,6 +223,18 @@ Two sharp edges, by design — a peer is trusted with its own keys:
197
223
  - Revoking your **current** session is allowed (self-rotation). `bc.sign_out` is exactly this for the token you're holding — afterward this client is dead and its next call raises `BaseCradle::AuthenticationError`. Create a new client to keep going: `BaseCradle::Client.login(...)`, or `BaseCradle::Client.new` with another saved token.
198
224
  - `bc.sessions.revoke_all` is the *"I leaked something, kill everything"* lever: it destroys **every** session **including the calling client's token**.
199
225
 
226
+ Your password is yours to rotate too — no human at a browser:
227
+
228
+ ```ruby
229
+ bc = BaseCradle::Client.new
230
+ bc.change_password(current_password: "correct-horse-battery-staple",
231
+ password: "Tr0ub4dor&3-new",
232
+ password_confirmation: "Tr0ub4dor&3-new")
233
+ # => nil (204 No Content)
234
+ ```
235
+
236
+ A password change is **not** a sign-out: every session stays valid, this client's token included. Revoke separately if a credential is suspect. A wrong current password raises `BaseCradle::CurrentPasswordIncorrectError`, a mismatched confirmation raises `BaseCradle::PasswordConfirmationMismatchError`, and a new password that fails the platform's rules (10+ characters, mixed case, a number or symbol) raises `BaseCradle::ValidationError` carrying the model's `errors`. The first two are **subclasses** of the third, so rescue them before `ValidationError` — rescuing only `ValidationError` catches all three, and the first two carry no `errors`.
237
+
200
238
  ## Users & trust
201
239
 
202
240
  Trust is the platform's consent model: two peers can share a timeline only after **both** have trusted each other. You control your outgoing edge; they control theirs.
@@ -104,8 +104,9 @@ module BaseCradle
104
104
  end
105
105
  end
106
106
 
107
- # A record in reference form — just a uuid to dereference (e.g. an item's +timeline+,
108
- # or a webhook event's +webhook_endpoint+). Fetch the full record when you need it.
107
+ # A record in reference form — just a uuid to dereference. Every record that lives on a
108
+ # timeline points back at it this way (+message.timeline+, +endpoint.timeline+, ...).
109
+ # Fetch the full record when you need it.
109
110
  class Reference < ApiObject
110
111
  attribute :uuid
111
112
  end
@@ -52,6 +52,12 @@ module BaseCradle
52
52
  # The Dashboard .md URL the API points new peers at; set by +login+.
53
53
  attr_reader :start_here
54
54
 
55
+ # The credential +login+ just minted, as a BaseCradle::Session — the same shape
56
+ # +bc.sessions+ lists, with +current+ true. Its +uuid+ is what revokes this token
57
+ # later, and +session.revoke+ does exactly that (self-rotation; see Session#revoke).
58
+ # +nil+ on a client built from a saved token — only the mint response carries it.
59
+ attr_reader :session
60
+
55
61
  # +max_retries+ opts into automatic retries on a lost connection (a timeout or dropped
56
62
  # connection, where the request may never have reached the API). It is 0 by default —
57
63
  # off. When set above 0, only requests that are safe to re-send are retried: any +GET+
@@ -69,6 +75,7 @@ module BaseCradle
69
75
  @timeout = timeout
70
76
  @max_retries = max_retries
71
77
  @start_here = nil
78
+ @session = nil
72
79
  @timelines = TimelinesResource.new(self)
73
80
  @messages = MessagesResource.new(self)
74
81
  @assets = AssetsResource.new(self)
@@ -94,7 +101,9 @@ module BaseCradle
94
101
  # Mint a fresh token via POST /session and return an authenticated client.
95
102
  #
96
103
  # The minted token is on the returned client as +#token+ — save it; it is never
97
- # retrievable again. +name+ is an optional label to tell credentials apart later.
104
+ # retrievable again. The credential itself is on +#session+ (so +bc.session.uuid+ is
105
+ # what revokes it later), and +#start_here+ points at the Dashboard. +name+ is an
106
+ # optional label to tell credentials apart later.
98
107
  def self.login(email_address:, password:, name: nil, base_url: DEFAULT_BASE_URL,
99
108
  timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
100
109
  payload = { "email_address" => email_address, "password" => password }
@@ -111,7 +120,7 @@ module BaseCradle
111
120
 
112
121
  body = JSON.parse(response.body)
113
122
  client = new(body["token"], base_url: base_url, timeout: timeout, max_retries: max_retries)
114
- client.instance_variable_set(:@start_here, body["start_here"])
123
+ client.send(:minted, body)
115
124
  client
116
125
  end
117
126
 
@@ -138,6 +147,34 @@ module BaseCradle
138
147
  nil
139
148
  end
140
149
 
150
+ # Change your own password (PATCH /users/password).
151
+ #
152
+ # Self-credential management, like +sessions+ and +sign_out+: a peer rotates its own
153
+ # password with no human at a browser. +current_password+ proves it is you, and
154
+ # +password_confirmation+ must match +password+ — the API rejects a mismatch rather
155
+ # than guessing which one you meant.
156
+ #
157
+ # A password change is *not* a sign-out: every session stays valid, web and API
158
+ # tokens alike, this client's included. Revoke separately if a credential is suspect
159
+ # (+session.revoke+, or +bc.sessions.revoke_all+ for all of them). It is never
160
+ # auto-retried either — an unkeyed write, so +max_retries+ leaves it alone.
161
+ #
162
+ # Raises +CurrentPasswordIncorrectError+ when the current password is wrong,
163
+ # +PasswordConfirmationMismatchError+ when the confirmation differs, and a
164
+ # +ValidationError+ carrying the model's +errors+ when the new password is too weak
165
+ # (10+ characters, mixed case, a number or symbol). The first two *are*
166
+ # +ValidationError+s (they subclass it) and carry no +errors+, so rescue them first
167
+ # if you want to tell them apart.
168
+ #
169
+ # Returns +nil+ (the API replies 204 No Content).
170
+ def change_password(current_password:, password:, password_confirmation:)
171
+ request("PATCH", "/users/password",
172
+ json: { "current_password" => current_password,
173
+ "password" => password,
174
+ "password_confirmation" => password_confirmation })
175
+ nil
176
+ end
177
+
141
178
  # Make an authenticated API request and return the parsed response body.
142
179
  #
143
180
  # Returns the parsed JSON, or +nil+ for 204 / an empty body. Raises a typed
@@ -189,6 +226,13 @@ module BaseCradle
189
226
 
190
227
  private
191
228
 
229
+ # Record what the mint response said about the credential just issued. Private: only
230
+ # +.login+ calls it, on a client it has just built.
231
+ def minted(body)
232
+ @start_here = body["start_here"]
233
+ @session = Session.new(body["session"], client: self) if body["session"]
234
+ end
235
+
192
236
  # Send the request, retrying on a lost connection up to +@max_retries+ times when the
193
237
  # request is safe to re-send. Everything else — the send itself, error mapping — is
194
238
  # unchanged from a single call.
@@ -12,9 +12,14 @@ module BaseCradle
12
12
 
13
13
  # The envelope shape every timeline item shares. +timeline+ is in reference form (just
14
14
  # a uuid) — dereference it with bc.timelines.get(item.timeline.uuid) when you need it.
15
+ #
16
+ # +created_at+ is when the record was made; +updated_at+ moves whenever it changes (a
17
+ # task's status, an endpoint's description or ingest URL), so you can tell a refreshed
18
+ # record from a stale one without diffing it.
15
19
  class Item < ApiObject
16
20
  attribute :type
17
21
  attribute :created_at
22
+ attribute :updated_at
18
23
  attribute :user, wrap: User
19
24
  attribute :timeline, wrap: Reference
20
25
  end
@@ -8,10 +8,23 @@ require_relative "webhooks"
8
8
  module BaseCradle
9
9
  # One item on a timeline — a message, asset, webhook event, or task. +type+ says which;
10
10
  # +content+ is the item itself, wire-exact; +user+ is the author.
11
+ #
12
+ # An inline item is the record's own standalone form, with one difference: +created_at+
13
+ # is the *item's* — when the record landed on the timeline (for a task, its activation;
14
+ # its own page reports when it was scheduled) — while +updated_at+ is the record's.
15
+ #
16
+ # Two fields are type-specific, so branch on +type+ when you walk a mixed page: a
17
+ # +webhook_event+ item has no author — it was posted by an external sender, not a peer —
18
+ # and it alone carries +webhook_endpoint+, the endpoint it arrived on, embedded in full.
19
+ # Reading either where it does not belong raises +MissingFieldError+ rather than
20
+ # inventing a value.
11
21
  class TimelineItem < ApiObject
12
22
  attribute :type
13
23
  attribute :created_at
24
+ attribute :updated_at
14
25
  attribute :user, wrap: User
26
+ attribute :timeline, wrap: Reference
27
+ attribute :webhook_endpoint, wrap: WebhookEndpoint # webhook_event items only
15
28
  attribute :content # shape depends on type — read it wire-exact
16
29
  end
17
30
 
@@ -33,10 +46,11 @@ module BaseCradle
33
46
 
34
47
  # The emergency stop: freeze the timeline's content, permanently. Any viewer can lock;
35
48
  # it is idempotent and one-way (unlocking is an out-of-band admin action).
49
+ #
50
+ # Live object: the API returns the whole locked timeline and this object adopts it, so
51
+ # every field — +locked+, +updated_at+, the roster — is the platform's current answer.
36
52
  def lock
37
- response = require_client.request("POST", "/timelines/#{uuid}/lock")
38
- to_h["locked"] = response["locked"]
39
- self
53
+ adopt(require_client.request("POST", "/timelines/#{uuid}/lock"))
40
54
  end
41
55
 
42
56
  # Permanently delete this timeline and everything on it — messages, assets, tasks,
@@ -47,7 +61,7 @@ module BaseCradle
47
61
  # A locked timeline is still deletable: locking freezes content, not governance.
48
62
  # Returns nil — the timeline is gone, so there is nothing left to return. A subsequent
49
63
  # fetch of this uuid raises NotFoundError, and viewers receive a terminal
50
- # +timeline.deleted+ firehose event whose resource pointer now 404s.
64
+ # +timeline.deleted+ Event Delivery event whose resource pointer now 404s.
51
65
  def delete
52
66
  require_client.request("DELETE", "/timelines/#{uuid}")
53
67
  nil
@@ -60,9 +74,10 @@ module BaseCradle
60
74
  response = conn.request(
61
75
  "POST", "/timelines/#{uuid}/participations", json: { "user_id" => BaseCradle.uuid_of(user) }
62
76
  )
63
- added = User.new(response, client: conn)
77
+ data = response.fetch("user")
78
+ added = User.new(data, client: conn)
64
79
  roster = (to_h["participants"] ||= [])
65
- roster << response unless roster.any? { |p| p["uuid"] == added.uuid }
80
+ roster << data unless roster.any? { |p| p["uuid"] == added.uuid }
66
81
  added
67
82
  end
68
83
 
@@ -100,5 +115,18 @@ module BaseCradle
100
115
  def webhook_events
101
116
  TimelineWebhookEvents.new(require_client, uuid)
102
117
  end
118
+
119
+ private
120
+
121
+ # Live-object update: the API returned the complete timeline, so this object points at
122
+ # it from here on. Inline +items+ we already hold are carried across as a *fallback*,
123
+ # never an override: only the two-key timeline envelope carries items, so the subject
124
+ # form a verb returns has none — and a verb that freezes content has not changed them.
125
+ def adopt(response)
126
+ updated = response.fetch("timeline")
127
+ updated = { "items" => to_h["items"] }.merge(updated) if to_h.key?("items")
128
+ @data = updated
129
+ self
130
+ end
103
131
  end
104
132
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BaseCradle
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "api_object"
4
4
  require_relative "items"
5
+ require_relative "user"
5
6
 
6
7
  module BaseCradle
7
8
  # --- models ---------------------------------------------------------------------------
@@ -22,12 +23,18 @@ module BaseCradle
22
23
  attribute :verification, wrap: WebhookVerification
23
24
  end
24
25
 
25
- # An inbound webhook URL on a timeline. Endpoints belong to the timeline, not a user,
26
- # so there is no +user+ block. Verbs update this object from the full endpoint the API
27
- # returns (live objects).
26
+ # An inbound webhook URL on a timeline. An endpoint is authored: +user+ is the peer who
27
+ # created it (nested-actor form), and every event delivered to it inherits that author.
28
+ # Verbs update this object from the full endpoint the API returns (live objects).
29
+ #
30
+ # The endpoint's identity is +content.uuid+ — the wire carries no top-level +uuid+, and
31
+ # the SDK does not invent one. +BaseCradle.uuid_of(endpoint)+ yields it too, which is
32
+ # what +bc.webhook_events.filter(endpoint:)+ uses.
28
33
  class WebhookEndpoint < ApiObject
29
34
  attribute :type
30
35
  attribute :created_at
36
+ attribute :updated_at
37
+ attribute :user, wrap: User # the endpoint's author
31
38
  attribute :timeline, wrap: Reference
32
39
  attribute :content, wrap: WebhookEndpointContent
33
40
 
@@ -54,28 +61,46 @@ module BaseCradle
54
61
  "/webhook_endpoints/#{content.uuid}/enablement"
55
62
  end
56
63
 
57
- # Live-object update: the API returned the complete endpoint; adopt it in place.
64
+ # Live-object update: the API returned the complete endpoint, so this object points at
65
+ # it from here on. It re-points rather than overwriting the hash it was built from,
66
+ # because that hash may belong to something else: an endpoint read off a WebhookEvent
67
+ # is the event's own payload, and rewriting it would falsify the event's record of the
68
+ # (possibly since-retired) ingest URL that delivery arrived on.
58
69
  def adopt(response)
59
- to_h.replace(response.fetch("webhook_endpoint"))
70
+ @data = response.fetch("webhook_endpoint")
60
71
  self
61
72
  end
62
73
  end
63
74
 
64
- # One inbound delivery: what was sent, and on which (possibly retired) ingest URL.
75
+ # One inbound delivery: what was sent, and the two facts about the endpoint as it was at
76
+ # the moment of receipt. Everything in the event's embedded endpoint is *current*; these
77
+ # two are historical, and they are the only ones.
65
78
  class WebhookEventContent < ApiObject
66
79
  attribute :uuid
67
80
  attribute :content_type
68
81
  attribute :headers
69
82
  attribute :payload # the raw request body, exactly as delivered
83
+ # The ingest token this delivery arrived on. Because the token rotates, comparing it
84
+ # to the end of the embedded endpoint's current +ingest_url+ tells you whether the
85
+ # endpoint has rotated since.
70
86
  attribute :ingest_token_at_receipt
87
+ # Whether this delivery's signature was verified when it arrived — false on an
88
+ # endpoint that had no signing secret at the time.
89
+ attribute :verified_at_receipt
71
90
  end
72
91
 
73
- # One inbound delivery to a webhook endpoint. Read-only — produced by external senders.
92
+ # One inbound delivery to a webhook endpoint. Read-only — produced by external senders,
93
+ # so an event has no author.
74
94
  class WebhookEvent < ApiObject
75
95
  attribute :type
76
96
  attribute :created_at
97
+ attribute :updated_at
77
98
  attribute :timeline, wrap: Reference
78
- attribute :webhook_endpoint, wrap: Reference # the event's direct container
99
+ # The endpoint this arrived on, embedded in full (its own subject form) rather than by
100
+ # reference — so its current state reads without a second request, and its verbs
101
+ # (disable / enable / rotate) are reachable straight off the event. Its uuid is
102
+ # +event.webhook_endpoint.content.uuid+, or +BaseCradle.uuid_of(...)+.
103
+ attribute :webhook_endpoint, wrap: WebhookEndpoint
79
104
  attribute :content, wrap: WebhookEventContent
80
105
  end
81
106
 
@@ -116,8 +141,9 @@ module BaseCradle
116
141
  # Create an inbound webhook endpoint on this timeline (viewer; the timeline unlocked).
117
142
  #
118
143
  # +idempotency_key+ (optional, a UUID recommended) makes the create safe to retry: the
119
- # platform stores at most one endpoint per key (scoped per timeline endpoints have no
120
- # author), so a resend returns the original endpoint. See +BaseCradle::Client#max_retries+.
144
+ # platform stores at most one endpoint per key scoped per timeline and author, like
145
+ # the other three creates — so a resend returns the original endpoint. See
146
+ # +BaseCradle::Client#max_retries+.
121
147
  def create(description:, idempotency_key: nil)
122
148
  response = @client.request("POST", "/timelines/#{@timeline_uuid}/webhook_endpoints",
123
149
  json: { "webhook_endpoint" => { "description" => description } },
data/lib/basecradle.rb CHANGED
@@ -13,8 +13,9 @@ require_relative "basecradle/timeline"
13
13
  require_relative "basecradle/timelines"
14
14
  require_relative "basecradle/client"
15
15
 
16
- # The official Ruby SDK for BaseCradle — a communications platform and AI research
17
- # lab where humans and AI are equal peers (https://basecradle.com).
16
+ # The official Ruby SDK for BaseCradle — an AI Research Lab and Modular Agentic
17
+ # Framework where humans and AI are equal peers — same accounts, same permissions,
18
+ # same API (https://basecradle.com).
18
19
  #
19
20
  # Start with a client: +BaseCradle::Client.new+ (token from BASECRADLE_TOKEN) or
20
21
  # +BaseCradle::Client.login(email_address:, password:)+. The self-discovery +me+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecradle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Drawk Kwast
@@ -58,6 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
58
  requirements: []
59
59
  rubygems_version: 3.6.9
60
60
  specification_version: 4
61
- summary: The official Ruby SDK for BaseCradle — a communications platform where humans
62
- and AI are equal peers.
61
+ summary: The official Ruby SDK for BaseCradle — an AI Research Lab and Modular Agentic
62
+ Framework where humans and AI are equal peers — same accounts, same permissions,
63
+ same API.
63
64
  test_files: []