slotlock 0.3.0 → 0.5.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 +252 -0
- package/README.md +226 -17
- package/agent-server.d.ts +51 -8
- package/agent-server.js +478 -36
- package/agent-store-backend.d.ts +6 -0
- package/agent-store-backend.js +771 -30
- package/application-api.d.ts +22 -1
- package/application-api.js +412 -3
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +20 -1
- package/config.js +33 -0
- package/dashboard.d.ts +61 -5
- package/dashboard.js +995 -139
- package/ddl.d.ts +5 -5
- package/ddl.js +590 -5
- package/engine.d.ts +5 -0
- package/engine.js +7 -4
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/index.d.ts +8 -2
- package/index.js +6 -1
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +14 -2
- package/maintenance.js +35 -3
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/openapi.json +8346 -3400
- package/package.json +1 -1
- package/rate-limit.d.ts +10 -1
- package/rate-limit.js +25 -2
- package/rest-api.d.ts +22 -0
- package/rest-api.js +160 -15
- package/self-host.d.ts +26 -3
- package/self-host.js +316 -19
- package/slotlock-manifest.schema.json +1 -0
- package/slotlock.manifest.json +2 -1
- package/store.d.ts +317 -8
- package/store.js +1005 -89
- package/types.d.ts +87 -0
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@ TypeScript library.
|
|
|
10
10
|
Slotlock 0.2.0 can occupy less time than an event was written with, so another booking can take the
|
|
11
11
|
rest: around daylight-saving changes, in any time zone that has them, and at the end of some series.
|
|
12
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
|
|
14
|
-
cases
|
|
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.
|
|
15
17
|
|
|
16
18
|
- A recurring event whose first occurrence spans a daylight-saving change gets that occurrence an
|
|
17
19
|
hour short: in spring, an hour of the written time is free to book.
|
|
@@ -75,7 +77,12 @@ docker compose exec slotlock slotlock resource add vehicle-42 --timezone Europe/
|
|
|
75
77
|
| `http://localhost:8080/a2a` | A2A 1.0, same token; card at `/.well-known/agent-card.json` |
|
|
76
78
|
| `http://localhost:8080/v1/…` | [REST](#rest-and-openapi): the tools, and the [application API](#application-api) for admin keys; described at `/openapi.json` |
|
|
77
79
|
| `http://localhost:8080/healthz` | Readiness, including the database |
|
|
80
|
+
| `http://localhost:8080/metrics` | Prometheus metrics, with `Authorization: Bearer <SLOTLOCK_METRICS_TOKEN>`; off without it |
|
|
78
81
|
|
|
82
|
+
- Metrics count calls by protocol, operation and outcome (`slotlock_calls_total`, with their
|
|
83
|
+
duration in `slotlock_call_duration_seconds`), confirmations, subscriptions, refusals, maintenance
|
|
84
|
+
and webhook deliveries. No label names a tenant, key or client. `createSlotlockMetrics` builds
|
|
85
|
+
them for a server of your own, from its hooks.
|
|
79
86
|
- The server runs as a role that owns nothing and cannot bypass row-level security; a one-off
|
|
80
87
|
`migrate` service applies the schema as the owner.
|
|
81
88
|
- `SLOTLOCK_AVAILABILITY` sets the default bookable hours. A resource can have its own
|
|
@@ -108,6 +115,9 @@ docker compose exec slotlock slotlock resource add vehicle-42 --timezone Europe/
|
|
|
108
115
|
and the rest wait their turn, so a tenant's writes waiting on its own locks leave the others to
|
|
109
116
|
other tenants and key lookups; a server with one tenant can set `tenant_transactions=10`. A
|
|
110
117
|
request whose transaction waits more than 5 seconds for a lock gets 503 `server_busy`.
|
|
118
|
+
With more than one server, `SLOTLOCK_SHARED_LIMITS=on` shares the tenant budgets (`tenant_*`,
|
|
119
|
+
`app_*`) through the database, so N servers allow a tenant its rate once, not N times; each server
|
|
120
|
+
checks its own buckets first, and goes on with them alone if the database cannot be reached.
|
|
111
121
|
`SLOTLOCK_LIMITS` changes the limits; behind a proxy, set `SLOTLOCK_TRUSTED_PROXY_HEADER` to the field it puts the
|
|
112
122
|
client's address in (`CF-Connecting-IP` behind Cloudflare) before the server takes traffic, or
|
|
113
123
|
every client shares the proxy's budget: the server logs a `client_address_field_ignored` warning
|
|
@@ -131,6 +141,37 @@ A rule is `rrule`, `startMinutes` and `durationMinutes` here and in `SLOTLOCK_AV
|
|
|
131
141
|
REST application API spells every field in snake_case, so there it is `rrule`, `start_minutes` and
|
|
132
142
|
`duration_minutes`. `slotlock --help` lists every setting.
|
|
133
143
|
|
|
144
|
+
#### Booking rules
|
|
145
|
+
|
|
146
|
+
A resource can also say what a booking on it must satisfy:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
slotlock rules set '{"minNoticeMinutes":120,"slotStepMinutes":30,"turnaroundMinutes":30,"enforceHours":true}' --resource vehicle-42
|
|
150
|
+
slotlock rules clear --resource vehicle-42
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Rule | A booking made now |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `minNoticeMinutes` | starts at least this far ahead |
|
|
156
|
+
| `maxAdvanceDays` | starts at most this far ahead |
|
|
157
|
+
| `slotStepMinutes` | starts on a step from local midnight in the resource's time zone |
|
|
158
|
+
| `turnaroundMinutes` | keeps this long free after it: its trailing buffer, which the database enforces |
|
|
159
|
+
| `enforceHours` | lies inside the resource's bookable hours |
|
|
160
|
+
| `checkInMinutes` | is claimed (`checkInReservation`) this long after its start, or maintenance releases it |
|
|
161
|
+
| `requireCertain` | lies in time every calendar sync recording coverage for the resource has read within a day |
|
|
162
|
+
|
|
163
|
+
- Holds, reservations, moves and agents' events are checked when they are made, by the database's
|
|
164
|
+
clock; a refusal names the rule (`booking_rule_violated` with `rule` in the store,
|
|
165
|
+
`booking_rule_min_notice`, `_max_advance`, `_slot_step`, `_outside_hours` or `_uncertain` to an
|
|
166
|
+
agent). Under `requireCertain`, a search (the store's and the tools') needs those syncs' coverage
|
|
167
|
+
too, read within a day, of the slot and its turnaround, so it offers no slot they leave unproven.
|
|
168
|
+
- A series is judged on its first occurrence for notice and advance, and on every occurrence for
|
|
169
|
+
step and hours. Imported calendar events are not checked: they happened elsewhere.
|
|
170
|
+
- Searches (`slotlock_find_next_available`, `slotlock_find_available_slots`, `findNextAvailableFor`)
|
|
171
|
+
offer only slots the rules allow, so an offered slot is one the write accepts.
|
|
172
|
+
- The REST application API sets them with `PUT /v1/resources/{resource_id}/booking-rules`, in
|
|
173
|
+
snake_case; the store with `setResourceBookingRules`.
|
|
174
|
+
|
|
134
175
|
### Without Docker
|
|
135
176
|
|
|
136
177
|
With Node.js 22.12+ and PostgreSQL 16 or newer, run each command as `npx slotlock` after
|
|
@@ -211,10 +252,33 @@ resources, so you can give each person API keys without running `slotlock key cr
|
|
|
211
252
|
3. Run `slotlock serve` and open `<SLOTLOCK_PUBLIC_URL>/dashboard`. With Compose, set the four in
|
|
212
253
|
`.env` (empty, the default, leaves the dashboard off) and run `docker compose up --detach`.
|
|
213
254
|
|
|
255
|
+
- Its pages: an Overview with the coming week's bookings, events and live holds across every
|
|
256
|
+
resource (each in its resource's time zone) and the latest changes agents made, named by key;
|
|
257
|
+
Resources, each with its own page for bookable hours, booking rules, pool, coming bookings and
|
|
258
|
+
activity; Waitlist, to see who waits and remove an entry; API keys, with the MCP and A2A
|
|
259
|
+
endpoints; Webhooks, to add, delete and re-enable endpoints and see which are failing; and
|
|
260
|
+
Settings.
|
|
261
|
+
Everything on them is read from the tenant's own data. `SLOTLOCK_DASHBOARD_NAME` sets what the
|
|
262
|
+
dashboard calls the deployment (Slotlock by default).
|
|
214
263
|
- Each person gets a tenant of their own, `github:<user id>`: the keys, resources and bookings in it
|
|
215
264
|
are theirs alone. Keys made with `slotlock key create` act in `SLOTLOCK_TENANT` instead.
|
|
216
|
-
-
|
|
217
|
-
|
|
265
|
+
- **For organizations instead of people**, set `SLOTLOCK_DASHBOARD_ORGS` to the GitHub organization
|
|
266
|
+
ids the dashboard serves (`gh api orgs/<login> --jq .id`). Each is then one tenant,
|
|
267
|
+
`github-org:<id>`, that its members share: keys, resources and hours. A person signs in to the
|
|
268
|
+
served organizations they are an active owner or member of (a billing manager is neither),
|
|
269
|
+
switching between them when there are several; someone in none of them is refused. Every member
|
|
270
|
+
has the same power: each can create, rotate and revoke the organization's keys, and the key list
|
|
271
|
+
says who created each one. Membership is read at each sign-in, so a person who leaves keeps
|
|
272
|
+
the dashboard until their session ends (12 hours at most), and keys they made work until revoked.
|
|
273
|
+
Removing an id from the list signs its sessions out and stops its keys at once. An organization
|
|
274
|
+
that restricts OAuth apps must approve the dashboard's app (its Settings, Third-party access), or
|
|
275
|
+
GitHub hides the membership. `SLOTLOCK_DASHBOARD_USERS` still says who may sign in at all, and a
|
|
276
|
+
key records the person who created it (`github:<user id>`): erasing that person's own tenant
|
|
277
|
+
leaves it on keys they made in an organization, which go with the organization's erasure. Only the
|
|
278
|
+
first 100 memberships GitHub lists, and the first 20 served organizations by login, are read.
|
|
279
|
+
- Sign-in uses `state` and PKCE, and keeps nothing from GitHub but the user id and login (and, with
|
|
280
|
+
organizations, the served ones the person belongs to): the GitHub token is used once and dropped.
|
|
281
|
+
It asks for no scope, or `read:org` with organizations.
|
|
218
282
|
- A session is a signed, HttpOnly cookie that lasts 12 hours. The allowlist is checked on every
|
|
219
283
|
request, dashboard and API alike: removing an id and restarting the server signs that person out
|
|
220
284
|
and stops every key in their tenant at once.
|
|
@@ -295,6 +359,9 @@ this one and read its session secret, which signs in as any tenant.
|
|
|
295
359
|
every client together, and the Worker logs a warning: add the ceiling before lowering that
|
|
296
360
|
binding's limit to one client's. The example configuration also removes query strings (a
|
|
297
361
|
callback's OAuth code) from the URLs Workers Logs keep.
|
|
362
|
+
- With organizations, set `SLOTLOCK_DASHBOARD_ORGS` on both the Worker and the server: the server
|
|
363
|
+
takes an organization's keys only while its own list names it, whether or not it serves the
|
|
364
|
+
dashboard.
|
|
298
365
|
- The server checks a dashboard tenant against `SLOTLOCK_DASHBOARD_USERS` only when it serves the
|
|
299
366
|
dashboard itself. With the dashboard on Workers, removing a person from the Worker's list signs
|
|
300
367
|
them out of the dashboard; to stop their keys, revoke them or erase the tenant.
|
|
@@ -481,6 +548,25 @@ uses the default, `[]` closes it); `setTenantAvailability({ tenantRef, rules })`
|
|
|
481
548
|
of a tenant. `createSlotlockStoreAgentBackend` answers from a resource's own hours and calls
|
|
482
549
|
`availabilityRules` only for resources without them.
|
|
483
550
|
|
|
551
|
+
Waitlists: `joinWaitlist({ tenantRef, resourceId | pool, start, end, holderRef, offerTtlMs })`
|
|
552
|
+
waits for a window that is taken now. When it comes free (a cancel, a released or expired hold, a
|
|
553
|
+
no-show), `offerWaitlist` gives it to the entries in the order they joined: the first whose window
|
|
554
|
+
is free, on its resource or the first free member of its pool, gets a hold for its holder, and the
|
|
555
|
+
feed says so (`waitlist.offered`, with `hold_id`). Confirm the hold to book it; let it go and the next
|
|
556
|
+
is offered. Cancels, releases and moves, whether an agent (MCP, A2A) or the application API makes
|
|
557
|
+
them, offer at once; maintenance offers what lapses on its own (an expired hold, a no-show) and
|
|
558
|
+
expires entries whose window has started (`waitlist.expired`). With `checkInMinutes` in a
|
|
559
|
+
resource's booking rules, maintenance releases a booking made since the rule was set that nobody
|
|
560
|
+
checked in that long after its start, or after it was made for a walk-in (`reservation.no_show`),
|
|
561
|
+
and the waitlist is offered its time. Check-in opens an hour before the start; moving a booking to
|
|
562
|
+
another start clears it. Leaving the waitlist lets an offer not yet confirmed go with it.
|
|
563
|
+
|
|
564
|
+
Calendar tools speak the standards: `emitVAvailability({ rules, timezone, uid })` writes rules as an
|
|
565
|
+
iCalendar VAVAILABILITY (RFC 7953) and `parseVAvailability(text)` reads one back as
|
|
566
|
+
`{ timezone, rules }`; `toJSCalendarEvent(event)` answers a calendar event as a JSCalendar Event
|
|
567
|
+
(RFC 8984). The API serves them at `/v1/resources/{resource_id}/availability/ical` and
|
|
568
|
+
`/v1/tenant/events/{event_id}/jscalendar`.
|
|
569
|
+
|
|
484
570
|
Writing rules:
|
|
485
571
|
|
|
486
572
|
- Create with `expectedRevision: 0`; update with the revision you last read and a new idempotency
|
|
@@ -780,6 +866,12 @@ export async function startCalendarServer(config: CalendarServerConfig) {
|
|
|
780
866
|
| `slotlock_list_resources` | Resources visible to the tenant | read-only |
|
|
781
867
|
| `slotlock_get_free_busy` | Busy intervals plus coverage certainty | read-only |
|
|
782
868
|
| `slotlock_find_next_available` | Earliest *certain* slot across resources | read-only |
|
|
869
|
+
| `slotlock_find_available_slots` | Up to 50 *certain* slots, earliest first, to offer a choice | read-only |
|
|
870
|
+
| `slotlock_hold_slots` | Hold up to 5 candidates for 30-900 seconds while a person chooses | write |
|
|
871
|
+
| `slotlock_release_slots` | Release a hold and the rest of its group | write |
|
|
872
|
+
| `slotlock_book_together` | Book the same time on 2-20 resources, all or none | write |
|
|
873
|
+
| `slotlock_book_pool` | Book any one free member of a pool (a vehicle class, a room type) | write |
|
|
874
|
+
| `slotlock_get_pool_availability` | How many of a pool's members are free across a window | read |
|
|
783
875
|
| `slotlock_list_events` | Events the caller created in a window | read-only |
|
|
784
876
|
| `slotlock_get_event` | One event the caller created | read-only |
|
|
785
877
|
| `slotlock_create_event` | Create an event of up to 3,660 days (idempotent) | write |
|
|
@@ -787,7 +879,33 @@ export async function startCalendarServer(config: CalendarServerConfig) {
|
|
|
787
879
|
| `slotlock_delete_event` | Tombstone at an expected revision | write, destructive |
|
|
788
880
|
|
|
789
881
|
A query window is at most 367 days. Each caller sees only the events it created, but everyone's
|
|
790
|
-
events count as busy time.
|
|
882
|
+
events count as busy time.
|
|
883
|
+
|
|
884
|
+
An agent offering a person several times holds them first, so none is taken while the person
|
|
885
|
+
decides: `slotlock_hold_slots` holds each free candidate in one group (and lists the taken ones in
|
|
886
|
+
`refused`), and `slotlock_create_event` with one `hold_id` books that time and releases the rest in
|
|
887
|
+
the same transaction. A hold belongs to the agent that took it: another agent can neither book nor
|
|
888
|
+
release it. One agent may have 20 live holds at once, holding at most 2,160 hours together in
|
|
889
|
+
`slotlock serve` (`SLOTLOCK_LIMITS`: `holder_holds`, `holder_held_hours`; `maxHoldsPerHolder` and
|
|
890
|
+
`maxHeldHoursPerHolder` in `createSlotlockStore`); past either it is refused, `hold_limit_reached` or
|
|
891
|
+
`held_time_limit_reached`. Unbooked holds expire by the database clock.
|
|
892
|
+
Notice was judged when the time was held, so booking the hold does not judge it again.
|
|
893
|
+
|
|
894
|
+
A booking that needs several resources at once (a van and its driver, a room and its projector)
|
|
895
|
+
uses `slotlock_find_available_slots` with `require_free` (every resource, or any n of them, free
|
|
896
|
+
together, each judged by its own hours and rules) and books with `slotlock_book_together`: one event
|
|
897
|
+
per resource in one transaction, all or none. A refusal names the `resource_id` that refused, on
|
|
898
|
+
MCP and A2A in the error beside its `code`, on REST in the problem. Bundles take their resources
|
|
899
|
+
in one order, so two racing over the same ones end with one booked, never a deadlock.
|
|
900
|
+
|
|
901
|
+
Rentals sell a class, not a plate. Put interchangeable resources in a pool
|
|
902
|
+
(`PUT /v1/resources/{id}/pool`, `setResourcePool`); agents see each resource's `pool`, list a pool's
|
|
903
|
+
members with `slotlock_list_resources` and `pool`, and book the class with `slotlock_book_pool`,
|
|
904
|
+
which takes the first member that is free and allows the booking, in one transaction: a class of
|
|
905
|
+
five sells exactly five for a time, however many race (`pool_full` after that). To see how many are
|
|
906
|
+
free, `slotlock_get_pool_availability` splits a window where that number changes; to see which,
|
|
907
|
+
search the members with `slotlock_find_available_slots` and `require_free: 1`: each slot names the
|
|
908
|
+
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
|
|
791
909
|
are no longer advertised.
|
|
792
910
|
|
|
793
911
|
### Errors
|
|
@@ -923,6 +1041,12 @@ never cached. W3C `traceparent`, `tracestate` and `baggage` reach your backend a
|
|
|
923
1041
|
| `GET /v1/resources` | `slotlock_list_resources` |
|
|
924
1042
|
| `GET /v1/free-busy` | `slotlock_get_free_busy` |
|
|
925
1043
|
| `GET /v1/next-available` | `slotlock_find_next_available` |
|
|
1044
|
+
| `GET /v1/available-slots` | `slotlock_find_available_slots` |
|
|
1045
|
+
| `POST /v1/slot-holds` | `slotlock_hold_slots` |
|
|
1046
|
+
| `DELETE /v1/slot-holds/{hold_id}` | `slotlock_release_slots` |
|
|
1047
|
+
| `POST /v1/bundles` | `slotlock_book_together` |
|
|
1048
|
+
| `POST /v1/pool-bookings` | `slotlock_book_pool` |
|
|
1049
|
+
| `GET /v1/pool-availability` | `slotlock_get_pool_availability` |
|
|
926
1050
|
| `GET /v1/events` | `slotlock_list_events` |
|
|
927
1051
|
| `POST /v1/events` | `slotlock_create_event` |
|
|
928
1052
|
| `GET /v1/events/{event_id}` | `slotlock_get_event` |
|
|
@@ -937,7 +1061,18 @@ curl "http://localhost:8080/v1/resources" -H "Authorization: Bearer $SLOTLOCK_AP
|
|
|
937
1061
|
`resource_ids=a&resource_ids=b`); `POST`, `PUT` and `PATCH` take a JSON body. Ids are in the path;
|
|
938
1062
|
percent-encode one that holds `/`.
|
|
939
1063
|
- Keys, scopes, `authorize` and the rate limit apply as over MCP. The body is the tool's output, or
|
|
940
|
-
|
|
1064
|
+
an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem (`application/problem+json`) with
|
|
1065
|
+
the matching HTTP status: `type` (a tag URI, one per code), `title`, `status`, `code`, and
|
|
1066
|
+
`error.code` as well, the body earlier clients read. An `overlap` names the `resource_id`, `start`
|
|
1067
|
+
and `end` it refused. A `Link: <…>; rel="describedby"` header points at these docs.
|
|
1068
|
+
- A write's `idempotency_key` may come as the `Idempotency-Key` header instead
|
|
1069
|
+
([draft 08](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)): a
|
|
1070
|
+
quoted Structured Field String (`Idempotency-Key: "booking-BK-42"`) or the bare key, equal to the
|
|
1071
|
+
argument when both are sent. A write that needs one and gets neither answers
|
|
1072
|
+
`400 idempotency_key_required`; the same key with another request, `422 idempotency_conflict`.
|
|
1073
|
+
An event write's key is kept 30 days (or `SLOTLOCK_RETENTION_DAYS`), then removed by maintenance,
|
|
1074
|
+
after which the key is new; a
|
|
1075
|
+
hold's until the hold would have expired, is released or is confirmed.
|
|
941
1076
|
- REST cannot ask a person, so a write listed in `confirmation` answers `428 confirmation_required`.
|
|
942
1077
|
- Free/busy and the next-available search read each resource they name once, however often it is
|
|
943
1078
|
named, and 10,000 busy intervals in all; the next-available search also expands 100,000 bookable
|
|
@@ -999,7 +1134,7 @@ curl -X PUT "http://localhost:8080/v1/reservations/BK-42" \
|
|
|
999
1134
|
`DELETE /v1/reservations/{hold_id}?revision=2`.
|
|
1000
1135
|
- `POST /v1/holds` and `DELETE /v1/tenant/events/…` take a retry identity as the `Idempotency-Key`
|
|
1001
1136
|
header or as `idempotency_key`. A retried hold answers the hold the first request made; the same
|
|
1002
|
-
key with another request is `
|
|
1137
|
+
key with another request is `422 idempotency_conflict`. Other routes are safe to retry as they
|
|
1003
1138
|
are, and refuse the header (`400 idempotency_key_not_supported`).
|
|
1004
1139
|
- Lists page by `limit` (up to 100) and `cursor`, the `next_cursor` of the page before, and read a
|
|
1005
1140
|
window of at most 367 days.
|
|
@@ -1017,6 +1152,58 @@ curl -X PUT "http://localhost:8080/v1/reservations/BK-42" \
|
|
|
1017
1152
|
`{ backend: createSlotlockStoreApplicationBackend(store, { maxTenantResources }), authorize }`.
|
|
1018
1153
|
A principal with `scopes` needs `admin`; then `authorize` decides.
|
|
1019
1154
|
|
|
1155
|
+
### Changes and webhooks
|
|
1156
|
+
|
|
1157
|
+
Every committed change to a tenant's holds, bookings and calendar events is recorded in the same
|
|
1158
|
+
transaction (by a database trigger, so no write path skips it), and nothing is recorded that did
|
|
1159
|
+
not commit. A change carries ids, times and states, never titles, descriptions or attendees.
|
|
1160
|
+
|
|
1161
|
+
- `GET /v1/changes?cursor=…` lists them oldest first as [CloudEvents 1.0](https://cloudevents.io)
|
|
1162
|
+
(`io.pylota.slotlock.hold.created`, `.reservation.cancelled`, `.event.updated`, …). Follow
|
|
1163
|
+
`next_cursor`, which comes back even when nothing is new: it never skips a change, even one whose
|
|
1164
|
+
transaction commits after a later one's. Changes are kept 30 days (`SLOTLOCK_RETENTION_DAYS`,
|
|
1165
|
+
`retentionDays` in `createSlotlockStore`, 1-3,650, for `serve` and `maintain` alike); a cursor
|
|
1166
|
+
that a pruned change came after, whichever server pruned it, answers `410 cursor_expired`. A change keeps who made
|
|
1167
|
+
it (`actor`) and a booking's or waitlist entry's reference: keep references opaque, not emails,
|
|
1168
|
+
when the feed is kept long. A shorter retention never shortens how long a retried write is
|
|
1169
|
+
answered as the first was: that stays at least 30 days.
|
|
1170
|
+
- `POST /v1/webhooks` with an https `url` (and `event_types`, optionally) sends each change from
|
|
1171
|
+
then on as a [Standard Webhooks](https://www.standardwebhooks.com) request: `webhook-id` (the
|
|
1172
|
+
change's id, the same on every retry), `webhook-timestamp` and `webhook-signature`, keyed by the
|
|
1173
|
+
`whsec_` secret the call returns once. Verify it with any Standard Webhooks library, and use
|
|
1174
|
+
`webhook-id` to drop a repeat: delivery is at least once.
|
|
1175
|
+
- Changes go to each webhook in order, one at a time, until one fails; that one is retried after
|
|
1176
|
+
5 seconds, 5 minutes, 30 minutes, 2, 5, then every 10 hours. After a day of failures, or a `410`,
|
|
1177
|
+
the webhook is disabled and the feed records `webhook.disabled`; `POST /v1/webhooks/{id}/enable`
|
|
1178
|
+
resumes it where it stopped, or answers `409 changes_pruned` when what it missed is older than the
|
|
1179
|
+
feed keeps (add it again and read what it missed from `/v1/changes`). The feed keeps every change
|
|
1180
|
+
an enabled webhook has still to receive, however old. At most 10 webhooks per tenant.
|
|
1181
|
+
- Each request has 15 seconds in all, and a tenant's pass 30 seconds; tenants are delivered 8 at
|
|
1182
|
+
a time, so a slow endpoint holds up only its own tenant.
|
|
1183
|
+
- History reads the same record backwards, with who made each change (the API key or agent) and,
|
|
1184
|
+
for a change of an existing booking or event, what it was before:
|
|
1185
|
+
`GET /v1/resources/{id}/history`, `GET /v1/reservations/{reference}/history`,
|
|
1186
|
+
`GET /v1/tenant/events/{id}/history`, newest first, paged by `before`. In the library, pass
|
|
1187
|
+
`actor` to `withTenant` and read `listHistory`. It is kept as long as the feed and erased with
|
|
1188
|
+
the tenant.
|
|
1189
|
+
- When time zone rules change (a new tzdata), a booking's instant and the wall time it was made for
|
|
1190
|
+
part: Winnipeg's 09:00 is no longer the instant stored. Slotlock keeps both, and maintenance finds
|
|
1191
|
+
future one-off events whose wall time moved: it reports each once (`event.timezone_shifted`, with
|
|
1192
|
+
the instant it has and the one its wall time now means). The library's `followZoneRules` option
|
|
1193
|
+
to `maintainSlotlockTenant` moves them instead, as a new revision, unless that overlaps another
|
|
1194
|
+
booking (`event.timezone_conflict`; it stays, for a person or an agent to decide). `/healthz`
|
|
1195
|
+
reports the `tzdata` the server runs.
|
|
1196
|
+
- Feed health: `GET /v1/resources/{id}/coverage` shows each source's window, when it was last read,
|
|
1197
|
+
its last error (`POST /v1/resources/{id}/coverage/{source}/failure` records one) and whether it is
|
|
1198
|
+
stale (not read for a day, so the time it covers is unproven, never free). A source that goes stale
|
|
1199
|
+
is put on the feed once, as `coverage.stale`, until it is read again. Retire a sync you have
|
|
1200
|
+
disconnected or replaced with `DELETE /v1/resources/{id}/coverage/{source}` (`deleteCoverage`):
|
|
1201
|
+
nothing waits on it after that, `requireCertain` included.
|
|
1202
|
+
- `slotlock serve` delivers (`SLOTLOCK_WEBHOOKS`): never to an address on a private network, which
|
|
1203
|
+
it checks when it connects, and following no redirect. `private` allows those, for testing.
|
|
1204
|
+
- In the library: `listChanges`, `createWebhookEndpoint` and the rest are store methods, and
|
|
1205
|
+
`startSlotlockWebhookDelivery` with `deliverSlotlockWebhooks` runs delivery in your own process.
|
|
1206
|
+
|
|
1020
1207
|
## Connect an agent
|
|
1021
1208
|
|
|
1022
1209
|
MCP and A2A are wire protocols, so an agent in any language connects with its own MCP or A2A client
|
|
@@ -1125,7 +1312,7 @@ export async function openLiveCalendar(options: LiveCalendarOptions) {
|
|
|
1125
1312
|
|
|
1126
1313
|
```sh
|
|
1127
1314
|
claude mcp add --transport http slotlock https://calendar.example.com/slotlock/mcp \
|
|
1128
|
-
--header "Authorization: Bearer $
|
|
1315
|
+
--header "Authorization: Bearer $SLOTLOCK_API_KEY"
|
|
1129
1316
|
```
|
|
1130
1317
|
|
|
1131
1318
|
**Claude API** (MCP connector, `anthropic-beta: mcp-client-2025-11-20`; the server must be public
|
|
@@ -1186,6 +1373,18 @@ Read-only tools link to `ui://slotlock/calendar`, a self-contained calendar view
|
|
|
1186
1373
|
supporting MCP Apps (`io.modelcontextprotocol/ui`) render in a sandbox. It makes no network requests
|
|
1187
1374
|
and never writes.
|
|
1188
1375
|
|
|
1376
|
+
A person can pick a slot in it. After `slotlock_find_available_slots` or a certain
|
|
1377
|
+
`slotlock_find_next_available`, the view shows a strip of days and each day's times by morning,
|
|
1378
|
+
afternoon and evening, or the one next available time. Picking a time and pressing *Book this time*
|
|
1379
|
+
sends one chat message naming the resource id and the slot's start and end. The agent books it with
|
|
1380
|
+
`slotlock_create_event`, through confirmation if `SLOTLOCK_CONFIRM_WRITES` asks for it. The message
|
|
1381
|
+
carries nothing else from the result. If the host refuses the message, the view shows the text to
|
|
1382
|
+
send instead. Write tools are marked `visibility: ["model"]`, so a host never lets a view call them.
|
|
1383
|
+
|
|
1384
|
+
The view takes its look from the host: its light or dark theme and its standard MCP Apps style
|
|
1385
|
+
variables (colours, fonts, sizes). It shows times in the person's locale, clock and time zone, with
|
|
1386
|
+
a 12h/24h switch.
|
|
1387
|
+
|
|
1189
1388
|
## Operations
|
|
1190
1389
|
|
|
1191
1390
|
Every tenant needs maintenance at least daily: it extends recurring events to the rolling horizon
|
|
@@ -1232,12 +1431,14 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1232
1431
|
(provider and sync authority) are kept until you erase the tenant. After the window a retried
|
|
1233
1432
|
command is evaluated afresh under its expected revision, and a pruned agent identity can be
|
|
1234
1433
|
created again. `retentionDays` (1-3650) changes the window; keep it longer than any client retries.
|
|
1235
|
-
- **Erasure.**
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1434
|
+
- **Erasure.** `slotlock tenant erase --confirm <tenant>`, or the store's `eraseTenant({ tenantRef })`,
|
|
1435
|
+
deletes everything the tenant owns in one transaction, waiting for its transactions in flight:
|
|
1436
|
+
resources, holds, bookings, events with their attendees and reminders, cancellations,
|
|
1437
|
+
idempotency records, coverage, API keys, and a personal tenant's dashboard records. Call it from
|
|
1438
|
+
your own erasure workflow; nothing is deleted table by table. Only the API keys' SHA-256 digests
|
|
1439
|
+
stay, with nothing about the tenant, so an erased key is never accepted again. The dashboard's
|
|
1440
|
+
other records hold no tenant and expire within 12 hours; a form record carries a SHA-256 digest
|
|
1441
|
+
of the GitHub user id that sent it, and an accepted write confirmation a digest of its nonce.
|
|
1241
1442
|
|
|
1242
1443
|
### Production checklist
|
|
1243
1444
|
|
|
@@ -1254,13 +1455,21 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1254
1455
|
9. Size live updates: at most `maxTotal` backend reads per `pollIntervalMs`.
|
|
1255
1456
|
10. Give each agent its own API key with the narrowest scope and an expiry.
|
|
1256
1457
|
11. After each deployment, check it end to end with a short-lived admin key, then revoke the key:
|
|
1257
|
-
`SLOTLOCK_URL=https://… SLOTLOCK_ADMIN_KEY=… node scripts/smoke-deployment.mjs
|
|
1458
|
+
`SLOTLOCK_URL=https://… SLOTLOCK_ADMIN_KEY=… node scripts/smoke-deployment.mjs`, from a clone
|
|
1459
|
+
of the repository, checks health,
|
|
1258
1460
|
discovery, MCP, A2A, holds and reservations (in a resource named `slotlock-smoke`, 400 days
|
|
1259
1461
|
ahead, cancelled after) and the 401, 409, 413 and 415 refusals.
|
|
1260
1462
|
12. Rate-limit every public server. `slotlock serve` does it per client address, key, tenant and
|
|
1261
1463
|
server (`SLOTLOCK_LIMITS`); behind a proxy, set `SLOTLOCK_TRUSTED_PROXY_HEADER` and firewall
|
|
1262
1464
|
the server so nothing reaches it except through that proxy. Embedding the
|
|
1263
1465
|
agent server, give it a `consumeRateLimit`. Watch serve's `refusals` log line for floods.
|
|
1466
|
+
13. Bound how long a transaction may run. A store over your own transaction (a savepoint per call)
|
|
1467
|
+
keeps its locks and its snapshot for as long as yours runs, and a hold you confirm inside it is
|
|
1468
|
+
judged by the statement's clock, not the transaction's. On PostgreSQL 17 and later, give the
|
|
1469
|
+
serving role a `transaction_timeout` above your longest legitimate transaction
|
|
1470
|
+
(`ALTER ROLE slotlock_app SET transaction_timeout = '60s'`): PostgreSQL ends one that runs
|
|
1471
|
+
longer, rolling back what it did, and closes its connection (the pool opens another). On 16, set `idle_in_transaction_session_timeout`
|
|
1472
|
+
and `statement_timeout`, which bound idle gaps and single statements but not the whole.
|
|
1264
1473
|
|
|
1265
1474
|
## Supported versions
|
|
1266
1475
|
|
|
@@ -1278,8 +1487,8 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1278
1487
|
Slotlock is availability maths, holds and reservations, events with recurrence, coverage-aware
|
|
1279
1488
|
free/busy, tenant isolation, iCalendar interchange, MCP and A2A with human confirmation and live
|
|
1280
1489
|
updates, an MCP Apps view, REST with the application API, scoped API keys, a GitHub sign-in
|
|
1281
|
-
dashboard, maintenance, and clients for five languages. [Hosted Slotlock](docs/hosted/README.md)
|
|
1282
|
-
|
|
1490
|
+
dashboard, maintenance, and clients for five languages. [Hosted Slotlock](docs/hosted/README.md),
|
|
1491
|
+
not open yet, will run it for you. Provider sync (Google, Microsoft, CalDAV), notifications to people, and a human
|
|
1283
1492
|
calendar app are yours to build around it. What each version promises is in
|
|
1284
1493
|
[VERSIONING.md](VERSIONING.md).
|
|
1285
1494
|
|
package/agent-server.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare const SLOTLOCK_A2A_TASK_STATES: readonly ["TASK_STATE_UNSPECIFIED
|
|
|
40
40
|
* Operation (MCP tool / A2A skill) names. They use only `[a-z_]`, so they are valid tool names for the
|
|
41
41
|
* Claude Messages API (`^[a-zA-Z0-9_-]{1,128}$`), OpenAI function calling and every MCP host.
|
|
42
42
|
*/
|
|
43
|
-
export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
|
|
43
|
+
export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_find_available_slots' | 'slotlock_hold_slots' | 'slotlock_release_slots' | 'slotlock_book_together' | 'slotlock_book_pool' | 'slotlock_get_pool_availability' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
|
|
44
44
|
/**
|
|
45
45
|
* Names earlier builds published, which every entry point still accepts and dispatches as the same
|
|
46
46
|
* operation; nothing advertises them. The dotted names came first, before tool names had to be
|
|
@@ -79,7 +79,13 @@ export interface SlotlockAgentInvocationContext {
|
|
|
79
79
|
export declare class SlotlockAgentOperationError extends Error {
|
|
80
80
|
readonly code: string;
|
|
81
81
|
readonly status: number;
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* What the refusal is about, beside its code (the resource a bundle was refused on, say): at most
|
|
84
|
+
* five snake_case names with text values of up to 200 characters; anything else is dropped. Every
|
|
85
|
+
* protocol carries them next to `code`.
|
|
86
|
+
*/
|
|
87
|
+
readonly details?: Readonly<Record<string, string>>;
|
|
88
|
+
constructor(code: string, status: number, details?: Record<string, string>);
|
|
83
89
|
}
|
|
84
90
|
export interface SlotlockAgentCalendarBackend {
|
|
85
91
|
listResources(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -101,6 +107,25 @@ export interface SlotlockAgentCalendarBackend {
|
|
|
101
107
|
name: string | null;
|
|
102
108
|
timezone: string | null;
|
|
103
109
|
} | null>;
|
|
110
|
+
/**
|
|
111
|
+
* Optional. Hold up to five candidate slots for this principal, as alternatives in one group
|
|
112
|
+
* (`slotlock_hold_slots`); a tool without it answers `holds_unavailable`.
|
|
113
|
+
*/
|
|
114
|
+
holdSlots?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
115
|
+
/** Optional. Book any one free member of a pool (`slotlock_book_pool`), else `pools_unavailable`. */
|
|
116
|
+
bookPool?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
117
|
+
/**
|
|
118
|
+
* Optional. How many of a pool's members are free across a window
|
|
119
|
+
* (`slotlock_get_pool_availability`), else `pools_unavailable`.
|
|
120
|
+
*/
|
|
121
|
+
getPoolAvailability?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
122
|
+
/**
|
|
123
|
+
* Optional. Book one interval on several resources in one transaction, all or none
|
|
124
|
+
* (`slotlock_book_together`); a tool without it answers `bundles_unavailable`.
|
|
125
|
+
*/
|
|
126
|
+
bookTogether?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
127
|
+
/** Optional. Release one of this principal's holds with its group (`slotlock_release_slots`). */
|
|
128
|
+
releaseSlots?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
104
129
|
}
|
|
105
130
|
/**
|
|
106
131
|
* OAuth 2.0 discovery for the MCP endpoint (MCP 2025-11-25 authorization, RFC 9728). Slotlock only
|
|
@@ -120,7 +145,7 @@ export interface SlotlockAgentServerOAuthOptions {
|
|
|
120
145
|
resourceDocumentation?: string;
|
|
121
146
|
}
|
|
122
147
|
/** The operations that change a calendar; the only ones a confirmation can guard. */
|
|
123
|
-
export type SlotlockAgentWriteOperation = Extract<SlotlockAgentOperation, 'slotlock_create_event' | 'slotlock_update_event' | 'slotlock_delete_event'>;
|
|
148
|
+
export type SlotlockAgentWriteOperation = Extract<SlotlockAgentOperation, 'slotlock_create_event' | 'slotlock_update_event' | 'slotlock_delete_event' | 'slotlock_book_together' | 'slotlock_book_pool'>;
|
|
124
149
|
/**
|
|
125
150
|
* Human confirmation before a write (MCP 2026-07-28 multi round-trip requests + form elicitation).
|
|
126
151
|
* A listed write first answers `input_required` with a yes/no form describing the change; it runs
|
|
@@ -141,6 +166,18 @@ export interface SlotlockAgentConfirmationOptions {
|
|
|
141
166
|
secrets: readonly (string | Uint8Array)[];
|
|
142
167
|
/** How long a confirmation prompt stays answerable, in seconds: 30-3,600, default 600. */
|
|
143
168
|
ttlSeconds?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Optional. Records an accepted confirmation, by the nonce its sealed state carries: true the first
|
|
171
|
+
* time, false for one already recorded, which the server then refuses (`confirmation_invalid`) so
|
|
172
|
+
* an acceptance runs its write once. Keep each nonce until `expiresAt`, across every server that
|
|
173
|
+
* shares the secrets (`createSlotlockAcceptanceRecords` does, in Postgres). Without it an accepted
|
|
174
|
+
* state can be sent again until it expires; it names the same principal, write and arguments, so
|
|
175
|
+
* the write's idempotency key or expected revision then decides.
|
|
176
|
+
*/
|
|
177
|
+
consumeAcceptance?: (acceptance: {
|
|
178
|
+
nonce: string;
|
|
179
|
+
expiresAt: Date;
|
|
180
|
+
}) => Promise<boolean>;
|
|
144
181
|
}
|
|
145
182
|
/**
|
|
146
183
|
* Live calendar updates over MCP 2026-07-28 `subscriptions/listen`. A client subscribes to calendar
|
|
@@ -318,6 +355,7 @@ export type SlotlockAgentOperationDispatchResult = {
|
|
|
318
355
|
ok: false;
|
|
319
356
|
status: number;
|
|
320
357
|
code: string;
|
|
358
|
+
details?: Readonly<Record<string, string>>;
|
|
321
359
|
};
|
|
322
360
|
/** Runtime type guard for the current operation names (legacy dotted names are not included). */
|
|
323
361
|
export declare function isSlotlockAgentOperation(name: unknown): name is SlotlockAgentOperation;
|
|
@@ -376,22 +414,27 @@ export declare function slotlockOpenApi(options?: {
|
|
|
376
414
|
application?: boolean;
|
|
377
415
|
}): Record<string, unknown>;
|
|
378
416
|
export declare function slotlockAgentTools(): {
|
|
379
|
-
name: "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_update_event";
|
|
380
|
-
title: "Create resource event" | "Delete resource event" | "Find next available interval" | "Get free-busy" | "Get resource event" | "List calendar resources" | "List resource events" | "Update resource event";
|
|
381
|
-
description: "Apply an expected-revision event patch and rematerialize occupancy atomically. With expected_resource_id, the resource you read the event on, an event since moved elsewhere is not found." | "Create an event of up to 3,660 days (a booking, lease or long rental); opaque occurrences use the database overlap arbiter." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found)." | "List bounded events intersecting a time window." | "List resources visible to the authenticated tenant principal." | "Read one tenant-scoped event by stable identifier." | "Return privacy-minimized busy intervals and explicit coverage certainty." | "Tombstone an event at an expected revision so stale sync cannot resurrect it.";
|
|
417
|
+
name: "slotlock_book_pool" | "slotlock_book_together" | "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_available_slots" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_get_pool_availability" | "slotlock_hold_slots" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_release_slots" | "slotlock_update_event";
|
|
418
|
+
title: "Book from a pool" | "Book resources together" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Update resource event";
|
|
419
|
+
description: "Apply an expected-revision event patch and rematerialize occupancy atomically. With expected_resource_id, the resource you read the event on, an event since moved elsewhere is not found." | "Book any one member of a pool (a vehicle class, a room type) for a time: members are tried in a fixed order and the first that is free and allows the booking is booked, in one transaction, so a class sells out exactly at its number of members. The event names the member booked. 409 pool_full when none is free; 404 pool_not_found when the pool has no member you may book. A retry with the same idempotency_key answers the same event." | "Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. A retry with the same idempotency_key answers the same events." | "Create an event of up to 3,660 days (a booking, lease or long rental); opaque occurrences use the database overlap arbiter." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found)." | "Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. A hold is yours alone, and you may have 20 live at once." | "How many members of a pool (a vehicle class, a room type) are free across a window, each only where its booking rules let a booking start: the window split where that number changes, each part with how many are free, and coverage certainty. Book one with slotlock_book_pool. 404 pool_not_found when the pool has no member you may book." | "List bounded events intersecting a time window." | "List resources visible to the authenticated tenant principal, with the pool each belongs to (a vehicle class, a room type). With pool, only that pool's members, in the order slotlock_book_pool tries them." | "List up to `limit` certain slots of the requested duration, earliest first across the resources, each one free of busy time and inside the resource's hours, to offer a person a choice. With require_free, only times when at least that many of the resources are free together, each slot naming them in resource_ids: every resource for a booking that needs them all (book it with slotlock_book_together), or any n of them. No slot is listed unless every resource's coverage is certain." | "Read one tenant-scoped event by stable identifier." | "Release one of your holds, and the rest of its group, at once instead of letting them expire. `released` is false when it was already gone." | "Return privacy-minimized busy intervals and explicit coverage certainty." | "Tombstone an event at an expected revision so stale sync cannot resurrect it.";
|
|
382
420
|
inputSchema: Readonly<Record<string, unknown>>;
|
|
383
421
|
outputSchema: Readonly<Record<string, unknown>>;
|
|
384
422
|
annotations: {
|
|
385
|
-
title: "Create resource event" | "Delete resource event" | "Find next available interval" | "Get free-busy" | "Get resource event" | "List calendar resources" | "List resource events" | "Update resource event";
|
|
423
|
+
title: "Book from a pool" | "Book resources together" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Update resource event";
|
|
386
424
|
readOnlyHint: boolean;
|
|
387
425
|
destructiveHint: boolean;
|
|
388
426
|
idempotentHint: true;
|
|
389
427
|
openWorldHint: boolean;
|
|
390
428
|
};
|
|
391
|
-
_meta
|
|
429
|
+
_meta: {
|
|
392
430
|
ui: {
|
|
393
431
|
resourceUri: string;
|
|
394
432
|
visibility: readonly ["model", "app"];
|
|
433
|
+
} | {
|
|
434
|
+
resourceUri: string;
|
|
435
|
+
visibility: readonly ["model"];
|
|
436
|
+
} | {
|
|
437
|
+
visibility: readonly ["model"];
|
|
395
438
|
};
|
|
396
439
|
};
|
|
397
440
|
}[];
|