slotlock 0.5.2 → 0.6.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.
- package/CHANGELOG.md +194 -0
- package/README.md +159 -66
- package/SECURITY.md +26 -13
- package/SPEC.md +10 -7
- package/agent-server.d.ts +47 -4
- package/agent-server.js +492 -147
- package/agent-store-backend.d.ts +9 -0
- package/agent-store-backend.js +83 -15
- package/api-keys.d.ts +12 -2
- package/api-keys.js +36 -7
- package/application-api.d.ts +7 -1
- package/application-api.js +70 -7
- package/assistant.js +2 -1
- package/booking-rules.d.ts +61 -9
- package/booking-rules.js +106 -16
- package/change-watch.d.ts +56 -0
- package/change-watch.js +213 -0
- package/dashboard.d.ts +3 -1
- package/dashboard.js +98 -15
- package/ddl.d.ts +21 -4
- package/ddl.js +555 -7
- package/event-feed.d.ts +54 -4
- package/event-feed.js +107 -8
- package/index.d.ts +8 -7
- package/index.js +5 -4
- package/maintenance.js +12 -10
- package/openapi.json +1309 -82
- package/package.json +1 -1
- package/rest-api.js +12 -0
- package/self-host.js +58 -6
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +35 -3
- package/store.js +447 -135
- package/types.d.ts +14 -1
- package/webhooks.d.ts +10 -2
- package/webhooks.js +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,200 @@
|
|
|
3
3
|
All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
|
|
4
4
|
versioning.
|
|
5
5
|
|
|
6
|
+
## 0.6.0 - 2026-10-29
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `invalid_arguments` from argument validation names the fields it refused: `issues`, up to 20
|
|
11
|
+
`{path, message}` (the field's path in the arguments, outermost first; `[]` is the arguments as a
|
|
12
|
+
whole). REST adds it to the problem details beside `code` (documented in openapi.json's `Error`),
|
|
13
|
+
MCP and A2A to `error`. Additive: `code` and the status are unchanged.
|
|
14
|
+
`invokeSlotlockAgentOperation` returns it too (`SlotlockArgumentIssue`).
|
|
15
|
+
- `slotlock serve` logs one `serving_as_schema_owner` warning at startup when its role owns the
|
|
16
|
+
`slotlock` schema: row-level security does not confine the owner on tables that do not force
|
|
17
|
+
it. It still serves.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Security: a coverage source's `source_id` could be confirmed by guessing. 0.5.3 named a source on the feed
|
|
22
|
+
by `src_` and 24 hex digits of an unkeyed SHA-256 of the tenant and the source, so anyone who saw
|
|
23
|
+
a change or a webhook and knew the tenant could test a guessed source offline: an address such as
|
|
24
|
+
`google:alice.smith@example.com`, say. A source's `source_id` is now random (`src_` and 32 hex
|
|
25
|
+
digits, 122 bits from `gen_random_uuid()`), stored with the source in
|
|
26
|
+
`calendar_coverage.source_id` when it is first recorded for the resource and kept until it is
|
|
27
|
+
deleted. Nothing outside the database can work it out; the tenant reads it beside the source from
|
|
28
|
+
`listCoverage` and `slotlock_get_coverage`.
|
|
29
|
+
- Security: changes an earlier release wrote still carried the source. Before 0.5.3 `coverage.stale` had the
|
|
30
|
+
source itself as `subject` and `data.source` (an iCalendar URL with its token) and the error read
|
|
31
|
+
from it as `data.last_error`, and `listChanges`, the history and webhooks that had not received it
|
|
32
|
+
served it until it was pruned. The upgrade renames every `coverage.*` change not in this release's
|
|
33
|
+
shape: `subject` and `data.source_id` become the source's new id, `source` and `last_error` are
|
|
34
|
+
removed. A change about a source deleted since is named `src_redacted`: nothing can match it any
|
|
35
|
+
more, and a made-up id would look like a source the tenant could find. A change an earlier
|
|
36
|
+
release's server (0.3-0.5) still running writes after the upgrade is put in the same shape as it
|
|
37
|
+
is inserted, by a trigger on the outbox (`slotlock.outbox_coverage_in_shape`), so none is stored
|
|
38
|
+
with its source. The read path redacts too: `listChanges`, `listRecentChanges`, `listHistory`,
|
|
39
|
+
webhook deliveries and `slotlockCloudEvent` never serve a `coverage.*` change with `source` or
|
|
40
|
+
`last_error`, and name one whose subject is not a `source_id` of this release `src_redacted`;
|
|
41
|
+
history by `subject` never finds a coverage change by any other name, so it cannot confirm a
|
|
42
|
+
guessed source either.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Breaking:** every `source_id` changes, and its shape is `src_` and 32 hex digits (was 24). A
|
|
47
|
+
reader that stored `source_id`s or `subject`s of `coverage.*` changes matches them again from
|
|
48
|
+
`listCoverage` / `GET /v1/resources/{id}/coverage`. The same source recorded for two resources,
|
|
49
|
+
or deleted and recorded again, has a different id each time.
|
|
50
|
+
- **Breaking:** changes already on the feed about coverage are rewritten in place by the upgrade, as
|
|
51
|
+
above; their positions and ids (`webhook-id`) do not change, so a webhook that already received
|
|
52
|
+
one is not sent it again, and one that has not receives the new shape.
|
|
53
|
+
|
|
54
|
+
- **Breaking:** `slotlockCoverageSourceId` throws (`coverage_source_id_unavailable`) instead of
|
|
55
|
+
returning 0.5.3's digest, which nothing carries any more and which would confirm a guessed source:
|
|
56
|
+
read `sourceId` from `listCoverage` (`source_id` of `slotlock_get_coverage`) instead.
|
|
57
|
+
- `slotlock migrate` (and `serve --migrate`) run as a role that does not own the schema, with no
|
|
58
|
+
`SLOTLOCK_MIGRATE_DATABASE_URL`, now fail with a first line that says to set
|
|
59
|
+
`SLOTLOCK_MIGRATE_DATABASE_URL` to the schema owner's URL, then the database's refusal.
|
|
60
|
+
- The README no longer opens with the 0.2.0 known issues: they are in
|
|
61
|
+
`docs/release/0.2.0-known-issues.md`, linked under "Supported versions".
|
|
62
|
+
- The hosted service page, its site page and SECURITY.md say the hosted service is not open and
|
|
63
|
+
that self-hosting is the only supported path today; SECURITY.md no longer invites testing it.
|
|
64
|
+
|
|
65
|
+
### Deprecated
|
|
66
|
+
|
|
67
|
+
- `slotlockCoverageSourceId`, which now only throws. It may be removed in 0.7.0.
|
|
68
|
+
|
|
69
|
+
### Upgrading
|
|
70
|
+
|
|
71
|
+
- **Breaking:** the schema is version 4. A 0.5 release refuses to migrate it ("Slotlock will not
|
|
72
|
+
migrate a schema a newer release migrated") and its server refuses to serve it, since its DDL would
|
|
73
|
+
put back the trigger that names a source by its guessable digest. Rolling back to 0.5.x needs a
|
|
74
|
+
restore of a backup taken before the upgrade.
|
|
75
|
+
- The migration adds `calendar_coverage.source_id` without rewriting the table (the column bare, its
|
|
76
|
+
random default set apart), gives each existing source an id under the migration's lock
|
|
77
|
+
(`calendar_coverage` is one row per source and resource), and adds its CHECK NOT VALID, validated
|
|
78
|
+
after the migration commits.
|
|
79
|
+
- Changes written from the upgrade on are put in shape as they are inserted. Those written before
|
|
80
|
+
it: once the structure commits, `applySchema` renames them in batches of 5,000 outbox ids by
|
|
81
|
+
primary key, each its own transaction, up to the last change when the pass started, read in a
|
|
82
|
+
short transaction of its own once the outbox writes in flight have ended. Where it got to is saved
|
|
83
|
+
with each batch (`schema_version.coverage_changes_renamed_through`): an interrupted pass goes on
|
|
84
|
+
from there, and the next migration reads only what was written since, until a pass after the
|
|
85
|
+
upgrade's finds nothing to rename and marks it done (`coverage_changes_renamed`). After that no
|
|
86
|
+
migration reads the outbox for it again.
|
|
87
|
+
- Locks: under a deployment role that forced RLS binds, each batch lifts FORCE from
|
|
88
|
+
`calendar_coverage` and `outbox` and puts it back before it commits, holding ACCESS EXCLUSIVE on
|
|
89
|
+
both for that batch (one range of 5,000 ids: milliseconds), so writes to either table wait that
|
|
90
|
+
long, once per batch, for every batch of the outbox on the upgrade and for the rows written since
|
|
91
|
+
on the passes after it. A superuser or BYPASSRLS role holds SHARE ROW EXCLUSIVE on the outbox
|
|
92
|
+
for the short transaction that reads the head (writes wait, reads do not), then only the row
|
|
93
|
+
locks of the rows it renames. `SLOTLOCK_CORE_DDL`, applied with psql, renames in its one transaction instead, with
|
|
94
|
+
the outbox under ACCESS EXCLUSIVE for the whole of it: a scan of the whole outbox on the upgrade.
|
|
95
|
+
- A migration step ended as a deadlock's victim (a caller's transaction that took the outbox, then a
|
|
96
|
+
coverage row, while a rename batch took them in the other order, say) is retried like one that
|
|
97
|
+
waited past its lock timeout, up to `attempts`, instead of failing.
|
|
98
|
+
|
|
99
|
+
## 0.5.3 - 2026-10-28
|
|
100
|
+
|
|
101
|
+
0.5.3 shipped breaking changes in a patch release, contrary to [VERSIONING.md](VERSIONING.md): the
|
|
102
|
+
two marked "Breaking" under **Changed**. Breaking changes will be minor releases from now on.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- Configuration changes on the feed and in history (RFC 0002 P1-5, P1-6), each in the change's own
|
|
107
|
+
transaction, by trigger: `resource.created`, `resource.updated` (booking and check-in rules,
|
|
108
|
+
pool, bookable hours, time zone or reference, with `before` and `after` of the fields that
|
|
109
|
+
changed), `resource.archived`, `resource.restored`, `resource.deleted`, `coverage.recorded` (a
|
|
110
|
+
source's first read) and `coverage.deleted`, `webhook.created`, `webhook.enabled` and
|
|
111
|
+
`webhook.deleted`, and `key.created`, `key.rotated` and `key.revoked`. Writing a value that is
|
|
112
|
+
already there records nothing. Rows carry ids and states: never a webhook's URL or secret, nor an
|
|
113
|
+
API key, its digest, prefix or name, nor a coverage source, which `coverage.*` changes name by
|
|
114
|
+
`source_id` (`src_` and 24 hex digits, a digest of the tenant and the source; `subject` too).
|
|
115
|
+
`slotlock_get_coverage` and `listCoverage` give each source's `source_id`, and
|
|
116
|
+
`slotlockCoverageSourceId` works it out. A webhook's `event_types` takes any of them, up to every
|
|
117
|
+
type (it took at most 20).
|
|
118
|
+
- Where a change came from: `withTenant` takes `origin` (`mcp`, `a2a`, `rest`, `application`,
|
|
119
|
+
`dashboard`, `dashboard-assistant`, `maintenance` or `library`, the default, also for a write
|
|
120
|
+
outside `withTenant`) and `keyId`, set by
|
|
121
|
+
each surface: the agent server per protocol, the application API, the dashboard and its
|
|
122
|
+
assistant, maintenance and webhook delivery. API key principals carry `keyId`, and the API key
|
|
123
|
+
store's `create`, `rotate` and `revoke` take `actor` and `origin`. The feed's new nullable
|
|
124
|
+
columns `origin` and `key_id` are in `listHistory` and `listChanges` (`origin`, `keyId`), the
|
|
125
|
+
history API (`origin`, `key_id`) and CloudEvents (extension attributes `slotlockorigin` and
|
|
126
|
+
`slotlockkeyid`). Rows written before keep null; an origin this release does not know reads as
|
|
127
|
+
null. `key_id` is pseudonymous and stays on a revoked key's changes for the retention. No address
|
|
128
|
+
or other personal data is recorded. The column's CHECK is added NOT VALID and validated after the
|
|
129
|
+
migration commits, so upgrading scans no rows under the table lock. An authenticator's key id
|
|
130
|
+
that is not a version 1-5 UUID is left out, as documented, rather than refusing the call.
|
|
131
|
+
- Live updates from the change feed (RFC 0002 P1-5). With `subscriptions.changes`
|
|
132
|
+
(`createSlotlockStoreChangeSource(store, sql)`, which `slotlock serve` sets), MCP resource
|
|
133
|
+
subscriptions follow each tenant's change feed instead of re-reading every 10 seconds: one reader
|
|
134
|
+
per tenant per process, shared by its subscriptions, polls the feed by cursor (one indexed query,
|
|
135
|
+
every 250 ms while changes come, `SLOTLOCK_FEED_BUSY_MS`, backing off to 1 s when quiet,
|
|
136
|
+
`SLOTLOCK_FEED_IDLE_MS`) and re-reads only the subscriptions a change touches, each at most once a
|
|
137
|
+
second, so a booking is announced within about a second. Nothing is added to the write path: no
|
|
138
|
+
trigger, no `NOTIFY`. A cursor pruned past announces every watched resource once. The re-read every
|
|
139
|
+
`pollIntervalMs` stays as the fallback and revalidates the caller (15 s with a feed), and the
|
|
140
|
+
subscription caps are unchanged. `slotlockChangeFeedHead` is where a reader starts.
|
|
141
|
+
- A2A streaming: the card declares `streaming`. `SendStreamingMessage` streams any skill's reply as
|
|
142
|
+
one message, and the `slotlock_watch_resources` skill streams a watch task with a status update
|
|
143
|
+
for each change to a watched resource's free/busy, from the same feed and caps as MCP
|
|
144
|
+
subscriptions, ending `TASK_STATE_COMPLETED` or, when access is withdrawn, `TASK_STATE_FAILED`.
|
|
145
|
+
Push notifications stay unsupported: webhooks deliver a tenant's changes to a URL.
|
|
146
|
+
- Benchmarks on every release (RFC 0002, P2-1): once a tag's suite passes, release.yml runs
|
|
147
|
+
`scripts/bench.mjs` against a PostgreSQL 16 service container, attaches `bench.json` and
|
|
148
|
+
`bench.md` to the GitHub release and rebuilds the site, whose new Benchmarks page shows the newest
|
|
149
|
+
results with the method and caveats. The job is informational: nothing waits on it and its failure
|
|
150
|
+
fails no release. The script now also measures free/busy reads and the non-overlapping workload
|
|
151
|
+
with durable commits off, records the PostgreSQL version, CPU count, clients and duration, and
|
|
152
|
+
writes both files with `--out DIR`.
|
|
153
|
+
- Booking rules: a leading buffer (`bufferBeforeMinutes`), the time kept free before each booking,
|
|
154
|
+
so two bookings stay the earlier one's trailing buffer plus the later one's leading buffer apart.
|
|
155
|
+
A reservation now occupies from `occupied_starts_at` (its start less `buffer_before_ms`, set by
|
|
156
|
+
a trigger) and the `slotlock_reservations_no_overlap` constraint keeps that time apart; free/busy,
|
|
157
|
+
searches, pool availability, holds and their reaping, coverage checks, series rolls and the
|
|
158
|
+
waitlist all read it.
|
|
159
|
+
- Booking rules: a step offset (`stepOffsetMinutes`, 0 to the step less one): with a 30-minute step
|
|
160
|
+
and 15, bookings start on :15 and :45, local time in the resource's zone, on writes and searches.
|
|
161
|
+
- Default booking rules for the tenant and for each pool (`setBookingRuleDefaults`,
|
|
162
|
+
`getBookingRuleDefaults`; `GET`/`PUT /v1/booking-rules`, `PUT /v1/pools/{pool}/booking-rules`):
|
|
163
|
+
a resource's rules as they apply (`effectiveBookingRules`, `effective_booking_rules`) are the
|
|
164
|
+
tenant's, overlaid by its pool's, overlaid by its own. A new tenant table under forced row-level
|
|
165
|
+
security, `booking_rule_defaults`, which erasure deletes. The dashboard's rules form has both new
|
|
166
|
+
rules and shows each inherited rule and where it comes from; an inherited on-or-off rule
|
|
167
|
+
(`enforceHours`, `requireCertain`) is a choice of default, on or off, so a resource can turn it
|
|
168
|
+
off. At most 1,000 pools a tenant have defaults (`pool_rules_limit_reached`). Merged rules are
|
|
169
|
+
read leniently: an offset under a shorter step applies modulo it, and an invalid field is ignored
|
|
170
|
+
and logged, never read as refusing every booking.
|
|
171
|
+
- Pool availability counts a member busy over its bookings' occupied time, buffers included, not
|
|
172
|
+
the buffers a new booking would keep.
|
|
173
|
+
|
|
174
|
+
### Upgrading
|
|
175
|
+
|
|
176
|
+
- The migration adds `reservations.buffer_before_ms` and `occupied_starts_at` and
|
|
177
|
+
`calendar_events.buffer_before_ms` without rewriting a row (existing rows keep no leading buffer,
|
|
178
|
+
so their occupied time is unchanged), adds their checks NOT VALID and validates them after it
|
|
179
|
+
commits, and rebuilds `slotlock_reservations_no_overlap` once.
|
|
180
|
+
- The rebuild holds an ACCESS EXCLUSIVE lock on `slotlock.reservations` for the whole index build,
|
|
181
|
+
about 15 seconds per million rows as measured in review: every read and write of reservations
|
|
182
|
+
waits until it ends. Schedule it, and for a very large table raise the migration's
|
|
183
|
+
`statementTimeoutMs` (`SlotlockMigrationOptions`, default 900,000 ms) so the build is not
|
|
184
|
+
cancelled.
|
|
185
|
+
- `slotlock migrate` applies row-level security to the new table and grants it to the serving role.
|
|
186
|
+
A library user runs `applyTenantRls`, then `grantApplicationRole` right after `applySchema` and
|
|
187
|
+
before serving traffic: until then every booking fails with a permission error on
|
|
188
|
+
`booking_rule_defaults`, which every booking reads.
|
|
189
|
+
|
|
190
|
+
### Changed
|
|
191
|
+
|
|
192
|
+
- Breaking: the feed now also records configuration changes, so a reader or webhook with no
|
|
193
|
+
`event_types` filter receives them too; filter by type to keep only bookings.
|
|
194
|
+
- Breaking: `coverage.stale` names its source by `source_id`, as `subject` and in `data`, in
|
|
195
|
+
place of `source`, and no longer carries `last_error`: a source can be an iCalendar URL with a
|
|
196
|
+
token or an address, and an error can quote it. `slotlock_get_coverage` still shows both to the tenant, with the
|
|
197
|
+
`source_id` to match a change to its source. Changes already on the feed keep the old payload
|
|
198
|
+
until they are pruned.
|
|
199
|
+
|
|
6
200
|
## 0.5.2 - 2026-10-28
|
|
7
201
|
|
|
8
202
|
### Added
|
package/README.md
CHANGED
|
@@ -5,44 +5,6 @@ reservable, answers "when is this free?" from weekly availability rules, and let
|
|
|
5
5
|
double bookings, however many writers race. Agents call it over MCP or A2A; your code calls it as a
|
|
6
6
|
TypeScript library.
|
|
7
7
|
|
|
8
|
-
## Known issues in 0.2.0
|
|
9
|
-
|
|
10
|
-
Slotlock 0.2.0 can occupy less time than an event was written with, so another booking can take the
|
|
11
|
-
rest: around daylight-saving changes, in any time zone that has them, and at the end of some series.
|
|
12
|
-
The library and `slotlock serve` are both affected (the last case below is in the library's
|
|
13
|
-
iCalendar readers only); the 0.1 releases are older still and unsupported. 0.3.0, released on
|
|
14
|
-
2026-10-27, fixes all six cases: upgrade (`npm install slotlock@0.3.0` or
|
|
15
|
-
`ghcr.io/pilotaai/slotlock:0.3.0`, then `slotlock migrate`, as the [changelog](CHANGELOG.md) says).
|
|
16
|
-
While you run 0.2.0, do not rely on it alone to refuse bookings in them.
|
|
17
|
-
|
|
18
|
-
- A recurring event whose first occurrence spans a daylight-saving change gets that occurrence an
|
|
19
|
-
hour short: in spring, an hour of the written time is free to book.
|
|
20
|
-
- Recurring events written through the store (the agent tools and the application API) lose the
|
|
21
|
-
repeated hour of an autumn change: times in it move an hour early, and a series whose first
|
|
22
|
-
occurrence spans it can store no occurrences, so all of its time is free.
|
|
23
|
-
- A stored recurring series cannot cancel or move an occurrence that falls in the skipped hour of a
|
|
24
|
-
spring change; the time it was moved to stays free.
|
|
25
|
-
- An occurrence that starts in the skipped hour and lasts longer than the skipped hour is cut short:
|
|
26
|
-
a 75-minute occurrence occupies 15 minutes.
|
|
27
|
-
- A timed recurring series with a date-only `UNTIL` ends at midnight UTC, so it can lose its last
|
|
28
|
-
day.
|
|
29
|
-
- An event read from iCalendar (`parseICalendarChanges`, `parseTrustedICalendarEvents` or the
|
|
30
|
-
worker pool) whose `DURATION` counts hours loses an hour across a spring change, whether it is a
|
|
31
|
-
single event, a series, an `RDATE` period or a moved occurrence: `DURATION:PT48H` occupies 47
|
|
32
|
-
hours.
|
|
33
|
-
|
|
34
|
-
0.1.0, 0.1.1 and 0.2.0 also pin zod 4.6.5 exactly, which has a high advisory
|
|
35
|
-
([GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5): validating an oversized
|
|
36
|
-
array can exhaust memory), which `npm audit` reports. While you run one of them, add this to your
|
|
37
|
-
application's `package.json` and run `npm install`:
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
"overrides": {"zod": "4.6.7"}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The full notice, with the versions affected, is in
|
|
44
|
-
[docs/release/0.2.0-known-issues.md](https://github.com/PILOTAAI/slotlock/blob/main/docs/release/0.2.0-known-issues.md).
|
|
45
|
-
|
|
46
8
|
## Why it is built for agents
|
|
47
9
|
|
|
48
10
|
- Resources own calendars, not users, and the tenant comes only from the authenticated caller.
|
|
@@ -155,6 +117,8 @@ slotlock rules clear --resource vehicle-42
|
|
|
155
117
|
| `minNoticeMinutes` | starts at least this far ahead |
|
|
156
118
|
| `maxAdvanceDays` | starts at most this far ahead |
|
|
157
119
|
| `slotStepMinutes` | starts on a step from local midnight in the resource's time zone |
|
|
120
|
+
| `stepOffsetMinutes` | starts this far past each step (0 to the step less one): with a 30-minute step, 15 starts bookings on :15 and :45 |
|
|
121
|
+
| `bufferBeforeMinutes` | keeps this long free before it: its leading buffer, which the database enforces |
|
|
158
122
|
| `turnaroundMinutes` | keeps this long free after it: its trailing buffer, which the database enforces |
|
|
159
123
|
| `enforceHours` | lies inside the resource's bookable hours |
|
|
160
124
|
| `checkInMinutes` | is claimed (`checkInReservation`) this long after its start, or maintenance releases it |
|
|
@@ -169,17 +133,32 @@ slotlock rules clear --resource vehicle-42
|
|
|
169
133
|
step and hours. Imported calendar events are not checked: they happened elsewhere.
|
|
170
134
|
- Searches (`slotlock_find_next_available`, `slotlock_find_available_slots`, `findNextAvailableFor`)
|
|
171
135
|
offer only slots the rules allow, so an offered slot is one the write accepts.
|
|
136
|
+
- Two bookings stay at least the earlier one's trailing buffer plus the later one's leading buffer
|
|
137
|
+
apart: each occupies from its start less its leading buffer to its end plus its trailing one, and
|
|
138
|
+
the exclusion constraint keeps those apart. Free/busy reports that time as busy.
|
|
172
139
|
- The REST application API sets them with `PUT /v1/resources/{resource_id}/booking-rules`, in
|
|
173
140
|
snake_case; the store with `setResourceBookingRules`.
|
|
141
|
+
- Defaults: rules for every resource of the tenant (`PUT /v1/booking-rules`) and for every member
|
|
142
|
+
of a pool (`PUT /v1/pools/{pool}/booking-rules`), read back with `GET /v1/booking-rules`; the
|
|
143
|
+
store's `setBookingRuleDefaults` and `getBookingRuleDefaults`. A resource's rules as they apply
|
|
144
|
+
(`effective_booking_rules`, `effectiveBookingRules`) are the tenant's defaults, overlaid by its
|
|
145
|
+
pool's, overlaid by its own: a rule set at a narrower level wins. `checkInMinutes` is a resource's
|
|
146
|
+
own only. The dashboard's resource page shows which rules a resource inherits, and from where.
|
|
147
|
+
- With no rule set anywhere, nothing changes: every rule is opt-in.
|
|
148
|
+
- A tenant has default rules for at most 1,000 pools (`pool_rules_limit_reached`, 409, for one
|
|
149
|
+
more). Merged rules are read leniently: a pool's `stepOffsetMinutes` under a resource's shorter
|
|
150
|
+
step applies modulo that step, and a field that is invalid on its own is ignored (and logged),
|
|
151
|
+
never read as refusing every booking.
|
|
174
152
|
|
|
175
153
|
### Without Docker
|
|
176
154
|
|
|
177
155
|
With Node.js 22.12+ and PostgreSQL 16 or newer, run each command as `npx slotlock` after
|
|
178
156
|
`npm install slotlock postgres`, or from a clone, built once, as `node dist/cli.js` (a clone does not
|
|
179
|
-
put `slotlock` on your `PATH`). Use two roles, as Compose does:
|
|
180
|
-
|
|
181
|
-
connects as the
|
|
182
|
-
superuser or a `BYPASSRLS` role, which row-level security does not
|
|
157
|
+
put `slotlock` on your `PATH`). Use two roles, as Compose does, so two URLs: `serve` connects as
|
|
158
|
+
`DATABASE_URL`, a role that owns nothing and cannot bypass row-level security, and `migrate`
|
|
159
|
+
connects as `SLOTLOCK_MIGRATE_DATABASE_URL`, the schema owner, and grants that role what it needs.
|
|
160
|
+
`serve` refuses to start as a superuser or a `BYPASSRLS` role, which row-level security does not
|
|
161
|
+
confine, and warns (`serving_as_schema_owner`) when its role owns the schema.
|
|
183
162
|
|
|
184
163
|
```sh
|
|
185
164
|
npm ci && npm run build
|
|
@@ -212,7 +191,7 @@ slotlock key revoke <id>
|
|
|
212
191
|
|
|
213
192
|
- A key (`slk_…`) is shown once and stored only as a SHA-256. Send it as `Authorization: Bearer`.
|
|
214
193
|
- It acts in the tenant it was created for (`SLOTLOCK_TENANT`), so one server serves many tenants.
|
|
215
|
-
- `read` covers the
|
|
194
|
+
- `read` covers the seven tools that only look; `write` covers create, update and delete; `admin`
|
|
216
195
|
covers the [application API](#application-api), which no agent needs. Default: read and write.
|
|
217
196
|
- `rotate` keeps the key's id, scopes and bookings; `revoke` stops it on its next request.
|
|
218
197
|
- A tenant may hold 100 active keys. The last six characters are a checksum secret scanners can
|
|
@@ -580,6 +559,9 @@ export async function bookHandover(store: SlotlockStore, tenantRef: string) {
|
|
|
580
559
|
}
|
|
581
560
|
```
|
|
582
561
|
|
|
562
|
+
Run every store method inside `withTenant`. A call outside it is not refused: it runs with no tenant
|
|
563
|
+
context, so as the application role forced row-level security shows it no rows.
|
|
564
|
+
|
|
583
565
|
`withTenant` validates the tenant, sets the context transaction-locally on the one connection every
|
|
584
566
|
callback call uses, supports nested savepoints, and restores the previous context. Do not issue a
|
|
585
567
|
standalone `set_config(..., true)` through a pool: its transaction ends before the next call.
|
|
@@ -632,6 +614,49 @@ Writing rules:
|
|
|
632
614
|
reference for a genuinely new event.
|
|
633
615
|
- `listCalendarEvents` and `listResources` return at most 1,000 rows and page with keyset cursors.
|
|
634
616
|
|
|
617
|
+
### Holds, external bookings and the feed
|
|
618
|
+
|
|
619
|
+
<!-- example: examples/store.ts#holds -->
|
|
620
|
+
```ts
|
|
621
|
+
export async function holdBookAndFollow(store: SlotlockStore, tenantRef: string, resourceId: string) {
|
|
622
|
+
// Inside withTenant: outside it the application role's row-level security shows no rows.
|
|
623
|
+
const booked = await store.withTenant(tenantRef, async (tenant) => {
|
|
624
|
+
// Hold a window for ten minutes while the customer pays, then turn it into a booking.
|
|
625
|
+
const held = await tenant.acquireHold({
|
|
626
|
+
tenantRef,
|
|
627
|
+
resourceId,
|
|
628
|
+
start: new Date('2027-04-05T09:00:00Z'),
|
|
629
|
+
end: new Date('2027-04-05T10:00:00Z'),
|
|
630
|
+
ttlMs: 10 * 60_000,
|
|
631
|
+
});
|
|
632
|
+
if (!held.ok) throw new Error(`not held: ${held.code}`);
|
|
633
|
+
const confirmed = await tenant.confirmHold(held.hold.id, { tenantRef });
|
|
634
|
+
if (!confirmed.ok) throw new Error(`hold not confirmed: ${confirmed.code}`);
|
|
635
|
+
|
|
636
|
+
// A booking your own system already has, by its reference there: revision 1 creates it.
|
|
637
|
+
const imported = await tenant.createExternalReservation({
|
|
638
|
+
tenantRef,
|
|
639
|
+
externalRef: 'BK-43',
|
|
640
|
+
resourceId,
|
|
641
|
+
start: new Date('2027-04-05T11:00:00Z'),
|
|
642
|
+
end: new Date('2027-04-05T12:00:00Z'),
|
|
643
|
+
bufferAfterMs: 15 * 60_000,
|
|
644
|
+
revision: 1,
|
|
645
|
+
});
|
|
646
|
+
if (!imported.ok) throw new Error(`BK-43 not booked: ${imported.code}`);
|
|
647
|
+
return { booking: confirmed.reservation, imported: imported.reservation };
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// What changed, oldest first, once committed: so in a withTenant of its own. Poll again with
|
|
651
|
+
// `after: feed.next`.
|
|
652
|
+
const feed = await store.withTenant(tenantRef, (tenant) =>
|
|
653
|
+
tenant.listChanges({ tenantRef, limit: 100 }),
|
|
654
|
+
);
|
|
655
|
+
if (!feed.ok) throw new Error(`feed not read: ${feed.code}`);
|
|
656
|
+
return { ...booked, feed };
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
635
660
|
### Recurring events
|
|
636
661
|
|
|
637
662
|
<!-- example: examples/store.ts#recurring -->
|
|
@@ -834,8 +859,10 @@ import {
|
|
|
834
859
|
type SlotlockStore,
|
|
835
860
|
createSlotlockAgentServer,
|
|
836
861
|
createSlotlockStoreAgentBackend,
|
|
862
|
+
createSlotlockStoreChangeSource,
|
|
837
863
|
} from 'slotlock';
|
|
838
864
|
import { createSlotlockNodeServer } from 'slotlock/node-server';
|
|
865
|
+
import type { Sql } from 'postgres';
|
|
839
866
|
|
|
840
867
|
const ALLOWED_OPERATIONS: ReadonlySet<SlotlockAgentOperation> = new Set([
|
|
841
868
|
'slotlock_list_resources',
|
|
@@ -849,6 +876,8 @@ const ALLOWED_OPERATIONS: ReadonlySet<SlotlockAgentOperation> = new Set([
|
|
|
849
876
|
|
|
850
877
|
export interface CalendarServerConfig {
|
|
851
878
|
store: SlotlockStore;
|
|
879
|
+
/** The store's client: live updates read the change feed's head with it. */
|
|
880
|
+
sql: Sql;
|
|
852
881
|
publicBaseUrl: string;
|
|
853
882
|
port: number;
|
|
854
883
|
verifyToken(token: string): Promise<SlotlockAgentPrincipal | null>;
|
|
@@ -869,7 +898,8 @@ export async function startCalendarServer(config: CalendarServerConfig) {
|
|
|
869
898
|
}
|
|
870
899
|
: {}),
|
|
871
900
|
resourceWindowDays: 90,
|
|
872
|
-
|
|
901
|
+
// Live updates follow each tenant's change feed: a booking is announced as it commits.
|
|
902
|
+
subscriptions: { changes: createSlotlockStoreChangeSource(config.store, config.sql) },
|
|
873
903
|
backend: createSlotlockStoreAgentBackend(config.store, {
|
|
874
904
|
availabilityRules: async () => [
|
|
875
905
|
{ rrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', startMinutes: 9 * 60, durationMinutes: 480 },
|
|
@@ -957,7 +987,9 @@ Rentals sell a class, not a plate. Put interchangeable resources in a pool
|
|
|
957
987
|
members with `slotlock_list_resources` and `pool`, and book the class with `slotlock_book_pool`,
|
|
958
988
|
which takes the first member that is free and allows the booking, in one transaction: a class of
|
|
959
989
|
five sells exactly five for a time, however many race (`pool_full` after that). To see how many are
|
|
960
|
-
free, `slotlock_get_pool_availability` splits a window where that number changes
|
|
990
|
+
free, `slotlock_get_pool_availability` splits a window where that number changes (counting each
|
|
991
|
+
member busy over its bookings' occupied time, buffers included, but not the buffers a new booking
|
|
992
|
+
would keep, so a member it counts free may still refuse a booking that abuts another); to see which,
|
|
961
993
|
search the members with `slotlock_find_available_slots` and `require_free: 1`: each slot names the
|
|
962
994
|
members free in `resource_ids`. `GET /v1/pools` lists the pools with their capacity. The older names `calendar.<verb>` and `calendar_<verb>` still work and
|
|
963
995
|
are no longer advertised.
|
|
@@ -971,7 +1003,7 @@ A refusal is a result the model can act on:
|
|
|
971
1003
|
|
|
972
1004
|
| Code | Meaning |
|
|
973
1005
|
| --- | --- |
|
|
974
|
-
| `invalid_arguments` | Input failed the schema, or a window is inverted or longer than 367 days |
|
|
1006
|
+
| `invalid_arguments` | Input failed the schema, or a window is inverted or longer than 367 days. A schema failure adds `issues`, up to 20 `{"path":[…],"message":"…"}`, each naming a field by its path in the arguments (`["rules",0,"start_minutes"]`; `[]` is the arguments as a whole): in `error` on MCP and A2A, beside `code` in REST's problem details |
|
|
975
1007
|
| `forbidden` | `authorize` refused the call |
|
|
976
1008
|
| `rate_limited` | `consumeRateLimit` refused the call |
|
|
977
1009
|
| `not_found`, `resource_not_found`, `event_not_found` | Not visible to this caller, or no such id: a resource id is the `id` `slotlock_list_resources` returns, not its `external_ref` |
|
|
@@ -998,7 +1030,7 @@ on stderr.
|
|
|
998
1030
|
| HTTP | `401` with `WWW-Authenticate`, `403` disallowed origin, `413`/`415` bad body, `429` rate limit (MCP, REST) |
|
|
999
1031
|
| MCP | `-32602` unknown tool, `arguments` that are not an object, or altered confirmation; `-32603` server fault; `-32601` unknown method |
|
|
1000
1032
|
| MCP 2026-07-28 | `-32020` header disagrees with body, `-32021` form elicitation not declared, `-32022` unsupported revision, `-31029` rate limit |
|
|
1001
|
-
| A2A | `-32602` malformed message or parameters, `-32029` rate limit (`RATE_LIMITED`, in the 200 envelope like every A2A error), `-32009` unsupported `A2A-Version`, `-32003` push notifications, `-32004`
|
|
1033
|
+
| A2A | `-32602` malformed message or parameters, `-32029` rate limit (`RATE_LIMITED`, in the 200 envelope like every A2A error), `-32009` unsupported `A2A-Version`, `-32003` push notifications, `-32004` task subscription or extended card, `-32001` task not found (the server keeps no tasks) |
|
|
1002
1034
|
|
|
1003
1035
|
### Authentication and OAuth discovery
|
|
1004
1036
|
|
|
@@ -1049,8 +1081,18 @@ On MCP 2026-07-28 each resource is also an MCP resource, `slotlock://resources/{
|
|
|
1049
1081
|
returns busy time and coverage for the next `resourceWindowDays` (30). `subscriptions/listen` streams
|
|
1050
1082
|
`notifications/resources/updated` when a resource's free/busy changes.
|
|
1051
1083
|
|
|
1052
|
-
-
|
|
1053
|
-
|
|
1084
|
+
- With `subscriptions.changes` (`createSlotlockStoreChangeSource(store, sql)`; `slotlock serve`
|
|
1085
|
+
sets it), changes come from the tenant's change feed: one reader per tenant per process, shared by
|
|
1086
|
+
all its subscriptions, polls the feed by cursor, one indexed query per read, under the tenant's
|
|
1087
|
+
row-level security. It reads again 250 ms after a read that found changes
|
|
1088
|
+
(`SLOTLOCK_FEED_BUSY_MS`, also the least gap between two reads) and backs off to once a second
|
|
1089
|
+
when quiet (`SLOTLOCK_FEED_IDLE_MS`), so a booking is announced within about a second. A change
|
|
1090
|
+
re-reads the subscriptions watching its resource, at most once a second each however many
|
|
1091
|
+
changes arrive, and each announces only what actually changed in its window. A cursor the feed
|
|
1092
|
+
has pruned past announces every watched resource once, then follows from now.
|
|
1093
|
+
- Each subscription is also re-read every `pollIntervalMs` (15 s with a feed, 10 s without one,
|
|
1094
|
+
which is then the only way changes are found), re-checking the caller each time, so a revoked key
|
|
1095
|
+
or what the feed does not carry (bookable hours) is caught.
|
|
1054
1096
|
- A subscription ends after `maxDurationMs` (15 minutes), on `shutdown()`, or when access is lost.
|
|
1055
1097
|
- At most 4 open per caller and 256 per server (`maxPerPrincipal`, `maxTotal`). `maxPerTenant`
|
|
1056
1098
|
caps one tenant's across all its callers (default `maxTotal`; `slotlock serve` sets 16), so a
|
|
@@ -1153,8 +1195,8 @@ waits for a confirmation.
|
|
|
1153
1195
|
| `POST /v1/resources/{resource_id}/archive` | Take it out of use; `…/restore` puts it back |
|
|
1154
1196
|
| `DELETE /v1/resources/{resource_id}` | Delete it with its history (`409 resource_in_use` while anything on it is live) |
|
|
1155
1197
|
| `GET /v1/resources/{resource_id}/busy?start=…&end=…` | Its occupied time, private resources included |
|
|
1156
|
-
| `PUT /v1/resources/{resource_id}/availability` |
|
|
1157
|
-
| `PUT /v1/availability` |
|
|
1198
|
+
| `PUT /v1/resources/{resource_id}/availability` | `{"rules":[…]}`: its own weekly hours; `"rules":[]` never bookable, `"rules":null` the server's |
|
|
1199
|
+
| `PUT /v1/availability` | `{"rules":[…]}`: every resource's weekly hours at once |
|
|
1158
1200
|
| `POST /v1/holds` | Hold a window for `ttl_seconds` (up to 7 days) |
|
|
1159
1201
|
| `GET /v1/holds/{hold_id}` | One live hold; `GET /v1/resources/{resource_id}/holds` lists them |
|
|
1160
1202
|
| `POST /v1/holds/{hold_id}/confirm` | Turn the hold into a reservation |
|
|
@@ -1208,12 +1250,40 @@ curl -X PUT "http://localhost:8080/v1/reservations/BK-42" \
|
|
|
1208
1250
|
|
|
1209
1251
|
### Changes and webhooks
|
|
1210
1252
|
|
|
1211
|
-
Every committed change to a tenant's holds, bookings
|
|
1212
|
-
transaction (by a database trigger, so no write path skips it), and nothing is recorded
|
|
1213
|
-
not commit. A change carries ids, times and states, never titles, descriptions
|
|
1253
|
+
Every committed change to a tenant's holds, bookings, calendar events and configuration is recorded
|
|
1254
|
+
in the same transaction (by a database trigger, so no write path skips it), and nothing is recorded
|
|
1255
|
+
that did not commit. A change carries ids, times and states, never titles, descriptions, attendees,
|
|
1256
|
+
webhook URLs or secrets.
|
|
1257
|
+
|
|
1258
|
+
- The types: `hold.created`, `.updated`, `.confirmed`, `.released`, `.expired`;
|
|
1259
|
+
`reservation.created`, `.updated`, `.cancelled`, `.checked_in`, `.no_show`; `event.created`,
|
|
1260
|
+
`.updated`, `.cancelled`, `.timezone_shifted`, `.timezone_conflict`; `waitlist.offered`,
|
|
1261
|
+
`.expired`; `coverage.stale`; and configuration: `resource.created`, `resource.updated` (booking
|
|
1262
|
+
and check-in rules, pool, bookable hours, time zone or reference, with `before` and `after` of
|
|
1263
|
+
only the fields that changed), `resource.archived`, `.restored`, `.deleted`; `coverage.recorded`
|
|
1264
|
+
(a source's first read; reading it again is not a change) and `coverage.deleted`. A `coverage.*`
|
|
1265
|
+
change names its source by `source_id` (also its `subject`), never the source itself, which can
|
|
1266
|
+
be an iCalendar URL with a token or an address: `src_` and 32 random hex digits, made when the
|
|
1267
|
+
source is first recorded for the resource and kept until it is deleted, so it cannot be worked
|
|
1268
|
+
out from the source. `slotlock_get_coverage` (`listCoverage`) gives it beside the source, to
|
|
1269
|
+
match a change to its source. A change about a source deleted before 0.6 named it is
|
|
1270
|
+
`src_redacted`;
|
|
1271
|
+
`webhook.created`, `.enabled`, `.disabled`, `.deleted` (the id and `event_types`, never the URL);
|
|
1272
|
+
`key.created`, `.rotated`, `.revoked` (the id, scopes and expiry, never the key, its digest,
|
|
1273
|
+
prefix or name). `SLOTLOCK_CHANGE_TYPES` lists them.
|
|
1274
|
+
- Each change records who made it (`actor`: the API key or agent), the API key's id (`key_id`) when
|
|
1275
|
+
a key made it, and where it came from (`origin`): `mcp`, `a2a`, `rest` (the REST agent routes),
|
|
1276
|
+
`application` (the application API), `dashboard`, `dashboard-assistant`, `maintenance`, or
|
|
1277
|
+
`library`: code calling the store itself, inside `withTenant` with no `origin` or outside it, and
|
|
1278
|
+
any write that names none. Never an address or anything else about a person. A change an earlier
|
|
1279
|
+
release recorded has neither (`null`).
|
|
1280
|
+
- `key_id` is pseudonymous: the key's id, never the key or its name. It stays on the changes the key
|
|
1281
|
+
made after the key is revoked or deleted, for as long as the feed keeps them (the retention below,
|
|
1282
|
+
and longer while an enabled webhook has still to receive them); erasing the tenant removes them.
|
|
1214
1283
|
|
|
1215
1284
|
- `GET /v1/changes?cursor=…` lists them oldest first as [CloudEvents 1.0](https://cloudevents.io)
|
|
1216
|
-
(`io.pylota.slotlock.hold.created`, `.reservation.cancelled`, `.event.updated`, …)
|
|
1285
|
+
(`io.pylota.slotlock.hold.created`, `.reservation.cancelled`, `.event.updated`, …), with the
|
|
1286
|
+
extension attributes `slotlockorigin` and `slotlockkeyid` when the change records them. Follow
|
|
1217
1287
|
`next_cursor`, which comes back even when nothing is new: it never skips a change, even one whose
|
|
1218
1288
|
transaction commits after a later one's. Changes are kept 30 days (`SLOTLOCK_RETENTION_DAYS`,
|
|
1219
1289
|
`retentionDays` in `createSlotlockStore`, 1-3,650, for `serve` and `maintain` alike); a cursor
|
|
@@ -1224,7 +1294,7 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1224
1294
|
- `POST /v1/webhooks` with an https `url` (and `event_types`, optionally) sends each change from
|
|
1225
1295
|
then on as a [Standard Webhooks](https://www.standardwebhooks.com) request: `webhook-id` (the
|
|
1226
1296
|
change's id, the same on every retry), `webhook-timestamp` and `webhook-signature`, keyed by the
|
|
1227
|
-
`whsec_` secret the call returns once. Verify it with any Standard Webhooks library, and use
|
|
1297
|
+
`whsec_` secret the call returns once. `event_types` takes any of the types above. Verify it with any Standard Webhooks library, and use
|
|
1228
1298
|
`webhook-id` to drop a repeat: delivery is at least once.
|
|
1229
1299
|
- Changes go to each webhook in order, one at a time, until one fails; that one is retried after
|
|
1230
1300
|
5 seconds, 5 minutes, 30 minutes, 2, 5, then every 10 hours. After a day of failures, or a `410`,
|
|
@@ -1234,11 +1304,14 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1234
1304
|
an enabled webhook has still to receive, however old. At most 10 webhooks per tenant.
|
|
1235
1305
|
- Each request has 15 seconds in all, and a tenant's pass 30 seconds; tenants are delivered 8 at
|
|
1236
1306
|
a time, so a slow endpoint holds up only its own tenant.
|
|
1237
|
-
- History reads the same record backwards, with who made each change (
|
|
1238
|
-
for a change of an existing booking or
|
|
1239
|
-
`GET /v1/resources/{id}/history
|
|
1240
|
-
`GET /v1/tenant/events/{id}/history`, newest first,
|
|
1241
|
-
|
|
1307
|
+
- History reads the same record backwards, with who made each change (`actor`, `key_id`), where
|
|
1308
|
+
from (`origin`) and, for a change of an existing booking, event or resource, what it was before:
|
|
1309
|
+
`GET /v1/resources/{id}/history` (its bookings, events and its own configuration),
|
|
1310
|
+
`GET /v1/reservations/{reference}/history`, `GET /v1/tenant/events/{id}/history`, newest first,
|
|
1311
|
+
paged by `before`. In the library, pass `actor`, `origin` and `keyId` to `withTenant` (`origin`
|
|
1312
|
+
defaults to the enclosing call's, else `library`; `createSlotlockApiKeyAuthenticator` gives the
|
|
1313
|
+
principal its `keyId`), and `actor` and `origin` to the API key store's `create`, `rotate` and
|
|
1314
|
+
`revoke`; read `listHistory`. It is kept as long as the feed and erased with
|
|
1242
1315
|
the tenant.
|
|
1243
1316
|
- When time zone rules change (a new tzdata), a booking's instant and the wall time it was made for
|
|
1244
1317
|
part: Winnipeg's 09:00 is no longer the instant stored. Slotlock keeps both, and maintenance finds
|
|
@@ -1247,7 +1320,8 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1247
1320
|
to `maintainSlotlockTenant` moves them instead, as a new revision, unless that overlaps another
|
|
1248
1321
|
booking (`event.timezone_conflict`; it stays, for a person or an agent to decide). `/healthz`
|
|
1249
1322
|
reports the `tzdata` the server runs.
|
|
1250
|
-
- Feed health: `GET /v1/resources/{id}/coverage` shows each source
|
|
1323
|
+
- Feed health: `GET /v1/resources/{id}/coverage` shows each source (and its `source_id`, as the feed
|
|
1324
|
+
names it), its window, when it was last read,
|
|
1251
1325
|
its last error (`POST /v1/resources/{id}/coverage/{source}/failure` records one) and whether it is
|
|
1252
1326
|
stale (not read for a day, so the time it covers is unproven, never free). A source that goes stale
|
|
1253
1327
|
is put on the feed once, as `coverage.stale`, until it is read again. Retire a sync you have
|
|
@@ -1421,6 +1495,15 @@ export async function listResourcesOverA2a(baseUrl: string, token: string) {
|
|
|
1421
1495
|
Send one `application/json` data part, `{"skill": "<id>", "arguments": {…}}`, with header
|
|
1422
1496
|
`A2A-Version: 1.0`. Each skill in the agent card has an example; replies are synchronous.
|
|
1423
1497
|
|
|
1498
|
+
The card declares `streaming`. `SendStreamingMessage` with any skill streams its reply as one
|
|
1499
|
+
message. With `slotlock_watch_resources` (`{"resource_ids": [...]}`, up to 20) it streams a watch:
|
|
1500
|
+
a task in `TASK_STATE_WORKING` naming the resources it can see, then a status update
|
|
1501
|
+
`{"resource_id", "uri"}` each time one's free/busy changes, from the same feed and under the same
|
|
1502
|
+
caps as MCP subscriptions, until `TASK_STATE_COMPLETED` (its duration, shutdown) or
|
|
1503
|
+
`TASK_STATE_FAILED` (access withdrawn). Closing the stream ends it; the task is not kept, so
|
|
1504
|
+
`SubscribeToTask` and `GetTask` do not find it. Push notifications are not offered: a tenant's
|
|
1505
|
+
changes reach a URL as signed webhooks.
|
|
1506
|
+
|
|
1424
1507
|
### MCP Apps
|
|
1425
1508
|
|
|
1426
1509
|
Read-only tools link to `ui://slotlock/calendar`, a self-contained calendar view that hosts
|
|
@@ -1506,7 +1589,9 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1506
1589
|
7. Monitor conflicts, refused rules, sync lag, capped maintenance runs, quota errors and confirmation outcomes.
|
|
1507
1590
|
8. Keep writes confirmation-gated wherever your policy needs a person (`slotlock serve`:
|
|
1508
1591
|
`SLOTLOCK_CONFIRM_WRITES`, `none` by default).
|
|
1509
|
-
9. Size live updates:
|
|
1592
|
+
9. Size live updates: one indexed feed query per second per tenant with an open subscription (four
|
|
1593
|
+
a second while its feed is busy), at most one backend read a second per subscription a change
|
|
1594
|
+
touches, and at most `maxTotal` backend reads per `pollIntervalMs`.
|
|
1510
1595
|
10. Give each agent its own API key with the narrowest scope and an expiry.
|
|
1511
1596
|
11. After each deployment, check it end to end with a short-lived admin key, then revoke the key:
|
|
1512
1597
|
`SLOTLOCK_URL=https://… SLOTLOCK_ADMIN_KEY=… node scripts/smoke-deployment.mjs`, from a clone
|
|
@@ -1517,7 +1602,13 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1517
1602
|
server (`SLOTLOCK_LIMITS`); behind a proxy, set `SLOTLOCK_TRUSTED_PROXY_HEADER` and firewall
|
|
1518
1603
|
the server so nothing reaches it except through that proxy. Embedding the
|
|
1519
1604
|
agent server, give it a `consumeRateLimit`. Watch serve's `refusals` log line for floods.
|
|
1520
|
-
13.
|
|
1605
|
+
13. On an upgrade, migrate first, then, as a library user, run `grantApplicationRole` again right
|
|
1606
|
+
after `applySchema` and before serving traffic: a release that adds a table (the leading-buffer
|
|
1607
|
+
release adds `booking_rule_defaults`, which every booking reads) fails every booking with a
|
|
1608
|
+
permission error until the serving role is granted it. `slotlock migrate` does both. A migration
|
|
1609
|
+
that rebuilds a constraint holds its table's lock for the whole build (see the CHANGELOG's
|
|
1610
|
+
Upgrading notes).
|
|
1611
|
+
14. Bound how long a transaction may run. A store over your own transaction (a savepoint per call)
|
|
1521
1612
|
keeps its locks and its snapshot for as long as yours runs, and a hold you confirm inside it is
|
|
1522
1613
|
judged by the statement's clock, not the transaction's. On PostgreSQL 17 and later, give the
|
|
1523
1614
|
serving role a `transaction_timeout` above your longest legitimate transaction
|
|
@@ -1536,13 +1627,15 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1536
1627
|
| PostgreSQL | 16, 17 and 18 (CI runs the database suites on each; Compose ships 18) |
|
|
1537
1628
|
| postgres (peer) | ^3.4.5 |
|
|
1538
1629
|
|
|
1630
|
+
0.2.0 and older can double-book around daylight-saving changes: [known issues](https://github.com/PILOTAAI/slotlock/blob/main/docs/release/0.2.0-known-issues.md#the-known-issues).
|
|
1631
|
+
|
|
1539
1632
|
## Project status and boundaries
|
|
1540
1633
|
|
|
1541
1634
|
Slotlock is availability maths, holds and reservations, events with recurrence, coverage-aware
|
|
1542
1635
|
free/busy, tenant isolation, iCalendar interchange, MCP and A2A with human confirmation and live
|
|
1543
1636
|
updates, an MCP Apps view, REST with the application API, scoped API keys, a GitHub sign-in
|
|
1544
|
-
dashboard, maintenance, and clients for five languages. [Hosted Slotlock](docs/hosted/README.md)
|
|
1545
|
-
not open
|
|
1637
|
+
dashboard, maintenance, and clients for five languages. [Hosted Slotlock](docs/hosted/README.md)
|
|
1638
|
+
is not open; self-hosting is the only supported path today. Provider sync (Google, Microsoft, CalDAV), notifications to people, and a human
|
|
1546
1639
|
calendar app are yours to build around it. What each version promises is in
|
|
1547
1640
|
[VERSIONING.md](VERSIONING.md).
|
|
1548
1641
|
|