slotlock 0.5.1 → 0.5.3
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 +122 -0
- package/README.md +152 -19
- package/SPEC.md +10 -7
- package/agent-server.d.ts +52 -4
- package/agent-server.js +479 -145
- 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 +67 -7
- package/assistant.d.ts +263 -0
- package/assistant.js +772 -0
- 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/config.d.ts +10 -0
- package/config.js +11 -0
- package/dashboard.d.ts +64 -1
- package/dashboard.js +687 -26
- package/ddl.d.ts +5 -5
- package/ddl.js +594 -6
- package/event-feed.d.ts +37 -4
- package/event-feed.js +74 -6
- package/index.d.ts +11 -8
- package/index.js +7 -5
- package/maintenance.d.ts +2 -1
- package/maintenance.js +33 -9
- package/openapi.json +1284 -82
- package/package.json +1 -1
- package/rate-limit.d.ts +8 -0
- package/rate-limit.js +20 -0
- package/self-host.d.ts +2 -2
- package/self-host.js +42 -7
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +132 -3
- package/store.js +482 -124
- package/types.d.ts +14 -1
- package/webhooks.d.ts +33 -2
- package/webhooks.js +102 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,128 @@
|
|
|
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.5.3 - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Configuration changes on the feed and in history (RFC 0002 P1-5, P1-6), each in the change's own
|
|
11
|
+
transaction, by trigger: `resource.created`, `resource.updated` (booking and check-in rules,
|
|
12
|
+
pool, bookable hours, time zone or reference, with `before` and `after` of the fields that
|
|
13
|
+
changed), `resource.archived`, `resource.restored`, `resource.deleted`, `coverage.recorded` (a
|
|
14
|
+
source's first read) and `coverage.deleted`, `webhook.created`, `webhook.enabled` and
|
|
15
|
+
`webhook.deleted`, and `key.created`, `key.rotated` and `key.revoked`. Writing a value that is
|
|
16
|
+
already there records nothing. Rows carry ids and states: never a webhook's URL or secret, nor an
|
|
17
|
+
API key, its digest, prefix or name, nor a coverage source, which `coverage.*` changes name by
|
|
18
|
+
`source_id` (`src_` and 24 hex digits, a digest of the tenant and the source; `subject` too).
|
|
19
|
+
`slotlock_get_coverage` and `listCoverage` give each source's `source_id`, and
|
|
20
|
+
`slotlockCoverageSourceId` works it out. A webhook's `event_types` takes any of them, up to every
|
|
21
|
+
type (it took at most 20).
|
|
22
|
+
- Where a change came from: `withTenant` takes `origin` (`mcp`, `a2a`, `rest`, `application`,
|
|
23
|
+
`dashboard`, `dashboard-assistant`, `maintenance` or `library`, the default, also for a write
|
|
24
|
+
outside `withTenant`) and `keyId`, set by
|
|
25
|
+
each surface: the agent server per protocol, the application API, the dashboard and its
|
|
26
|
+
assistant, maintenance and webhook delivery. API key principals carry `keyId`, and the API key
|
|
27
|
+
store's `create`, `rotate` and `revoke` take `actor` and `origin`. The feed's new nullable
|
|
28
|
+
columns `origin` and `key_id` are in `listHistory` and `listChanges` (`origin`, `keyId`), the
|
|
29
|
+
history API (`origin`, `key_id`) and CloudEvents (extension attributes `slotlockorigin` and
|
|
30
|
+
`slotlockkeyid`). Rows written before keep null; an origin this release does not know reads as
|
|
31
|
+
null. `key_id` is pseudonymous and stays on a revoked key's changes for the retention. No address
|
|
32
|
+
or other personal data is recorded. The column's CHECK is added NOT VALID and validated after the
|
|
33
|
+
migration commits, so upgrading scans no rows under the table lock. An authenticator's key id
|
|
34
|
+
that is not a version 1-5 UUID is left out, as documented, rather than refusing the call.
|
|
35
|
+
- Live updates from the change feed (RFC 0002 P1-5). With `subscriptions.changes`
|
|
36
|
+
(`createSlotlockStoreChangeSource(store, sql)`, which `slotlock serve` sets), MCP resource
|
|
37
|
+
subscriptions follow each tenant's change feed instead of re-reading every 10 seconds: one reader
|
|
38
|
+
per tenant per process, shared by its subscriptions, polls the feed by cursor (one indexed query,
|
|
39
|
+
every 250 ms while changes come, `SLOTLOCK_FEED_BUSY_MS`, backing off to 1 s when quiet,
|
|
40
|
+
`SLOTLOCK_FEED_IDLE_MS`) and re-reads only the subscriptions a change touches, each at most once a
|
|
41
|
+
second, so a booking is announced within about a second. Nothing is added to the write path: no
|
|
42
|
+
trigger, no `NOTIFY`. A cursor pruned past announces every watched resource once. The re-read every
|
|
43
|
+
`pollIntervalMs` stays as the fallback and revalidates the caller (15 s with a feed), and the
|
|
44
|
+
subscription caps are unchanged. `slotlockChangeFeedHead` is where a reader starts.
|
|
45
|
+
- A2A streaming: the card declares `streaming`. `SendStreamingMessage` streams any skill's reply as
|
|
46
|
+
one message, and the `slotlock_watch_resources` skill streams a watch task with a status update
|
|
47
|
+
for each change to a watched resource's free/busy, from the same feed and caps as MCP
|
|
48
|
+
subscriptions, ending `TASK_STATE_COMPLETED` or, when access is withdrawn, `TASK_STATE_FAILED`.
|
|
49
|
+
Push notifications stay unsupported: webhooks deliver a tenant's changes to a URL.
|
|
50
|
+
- Benchmarks on every release (RFC 0002, P2-1): once a tag's suite passes, release.yml runs
|
|
51
|
+
`scripts/bench.mjs` against a PostgreSQL 16 service container, attaches `bench.json` and
|
|
52
|
+
`bench.md` to the GitHub release and rebuilds the site, whose new Benchmarks page shows the newest
|
|
53
|
+
results with the method and caveats. The job is informational: nothing waits on it and its failure
|
|
54
|
+
fails no release. The script now also measures free/busy reads and the non-overlapping workload
|
|
55
|
+
with durable commits off, records the PostgreSQL version, CPU count, clients and duration, and
|
|
56
|
+
writes both files with `--out DIR`.
|
|
57
|
+
- Booking rules: a leading buffer (`bufferBeforeMinutes`), the time kept free before each booking,
|
|
58
|
+
so two bookings stay the earlier one's trailing buffer plus the later one's leading buffer apart.
|
|
59
|
+
A reservation now occupies from `occupied_starts_at` (its start less `buffer_before_ms`, set by
|
|
60
|
+
a trigger) and the `slotlock_reservations_no_overlap` constraint keeps that time apart; free/busy,
|
|
61
|
+
searches, pool availability, holds and their reaping, coverage checks, series rolls and the
|
|
62
|
+
waitlist all read it.
|
|
63
|
+
- Booking rules: a step offset (`stepOffsetMinutes`, 0 to the step less one): with a 30-minute step
|
|
64
|
+
and 15, bookings start on :15 and :45, local time in the resource's zone, on writes and searches.
|
|
65
|
+
- Default booking rules for the tenant and for each pool (`setBookingRuleDefaults`,
|
|
66
|
+
`getBookingRuleDefaults`; `GET`/`PUT /v1/booking-rules`, `PUT /v1/pools/{pool}/booking-rules`):
|
|
67
|
+
a resource's rules as they apply (`effectiveBookingRules`, `effective_booking_rules`) are the
|
|
68
|
+
tenant's, overlaid by its pool's, overlaid by its own. A new tenant table under forced row-level
|
|
69
|
+
security, `booking_rule_defaults`, which erasure deletes. The dashboard's rules form has both new
|
|
70
|
+
rules and shows each inherited rule and where it comes from; an inherited on-or-off rule
|
|
71
|
+
(`enforceHours`, `requireCertain`) is a choice of default, on or off, so a resource can turn it
|
|
72
|
+
off. At most 1,000 pools a tenant have defaults (`pool_rules_limit_reached`). Merged rules are
|
|
73
|
+
read leniently: an offset under a shorter step applies modulo it, and an invalid field is ignored
|
|
74
|
+
and logged, never read as refusing every booking.
|
|
75
|
+
- Pool availability counts a member busy over its bookings' occupied time, buffers included, not
|
|
76
|
+
the buffers a new booking would keep.
|
|
77
|
+
|
|
78
|
+
### Upgrading
|
|
79
|
+
|
|
80
|
+
- The migration adds `reservations.buffer_before_ms` and `occupied_starts_at` and
|
|
81
|
+
`calendar_events.buffer_before_ms` without rewriting a row (existing rows keep no leading buffer,
|
|
82
|
+
so their occupied time is unchanged), adds their checks NOT VALID and validates them after it
|
|
83
|
+
commits, and rebuilds `slotlock_reservations_no_overlap` once.
|
|
84
|
+
- The rebuild holds an ACCESS EXCLUSIVE lock on `slotlock.reservations` for the whole index build,
|
|
85
|
+
about 15 seconds per million rows as measured in review: every read and write of reservations
|
|
86
|
+
waits until it ends. Schedule it, and for a very large table raise the migration's
|
|
87
|
+
`statementTimeoutMs` (`SlotlockMigrationOptions`, default 900,000 ms) so the build is not
|
|
88
|
+
cancelled.
|
|
89
|
+
- `slotlock migrate` applies row-level security to the new table and grants it to the serving role.
|
|
90
|
+
A library user runs `applyTenantRls`, then `grantApplicationRole` right after `applySchema` and
|
|
91
|
+
before serving traffic: until then every booking fails with a permission error on
|
|
92
|
+
`booking_rule_defaults`, which every booking reads.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- The feed now also records configuration changes, so a reader or webhook with no `event_types`
|
|
97
|
+
filter receives them too; filter by type to keep only bookings.
|
|
98
|
+
- `coverage.stale` names its source by `source_id`, as `subject` and in `data`, in place of `source`,
|
|
99
|
+
and no longer carries `last_error`: a source can be an iCalendar URL with a token or an address,
|
|
100
|
+
and an error can quote it. `slotlock_get_coverage` still shows both to the tenant, with the
|
|
101
|
+
`source_id` to match a change to its source. Changes already on the feed keep the old payload
|
|
102
|
+
until they are pruned.
|
|
103
|
+
|
|
104
|
+
## 0.5.2 - 2026-10-28
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
- The dashboard assistant: with `SLOTLOCK_ASSISTANT_SECRET`, an organization's owners connect a
|
|
109
|
+
model of their own in Settings (the Anthropic Messages API, or OpenAI-compatible chat
|
|
110
|
+
completions: OpenAI, OpenRouter, vLLM, Ollama, LM Studio), with a connection test, and every
|
|
111
|
+
member can operate Slotlock by chat on a new Assistant page. The server runs the tool loop (up to
|
|
112
|
+
8 model calls a message) with a fixed list of agent tools: reads run through the agent operation
|
|
113
|
+
boundary, and every write is a proposed change the member approves on a form before it runs once,
|
|
114
|
+
recorded as `dashboard-assistant:github:<user id>`, whose card shows the change in full under the
|
|
115
|
+
confirmation question's sentence. A proposal takes no series-write slot or rate token until it
|
|
116
|
+
runs, a message's reads share a budget, maintenance deletes every tenant's expired conversations
|
|
117
|
+
(`slotlock.sweep_assistant_conversations`, over the `assistant_tenants` list), and a new provider
|
|
118
|
+
or base URL takes the key again. The API key is sealed with AES-256-GCM, bound to the
|
|
119
|
+
tenant, and only its last four characters are shown. Two tenant tables under forced row-level
|
|
120
|
+
security, `assistant_settings` and `assistant_conversations`, which erasure deletes.
|
|
121
|
+
`createSlotlockRateLimiter` is the per-key token bucket it limits members' messages with, and
|
|
122
|
+
organization sessions now remember each organization's role (owners are admins). As webhooks
|
|
123
|
+
do, it refuses a model URL whose host is, or resolves to, a loopback, private, link-local, CGNAT,
|
|
124
|
+
unique-local or metadata address, when saved and at each connection (the resolved address is
|
|
125
|
+
checked as the socket connects, against DNS rebinding); `SLOTLOCK_ASSISTANT_PRIVATE_URLS=on`
|
|
126
|
+
allows them for a self-hosted model. `createSlotlockGuardedFetch` is that `fetch`.
|
|
127
|
+
|
|
6
128
|
## 0.5.1 - 2026-10-28
|
|
7
129
|
|
|
8
130
|
### Fixed
|
package/README.md
CHANGED
|
@@ -155,6 +155,8 @@ slotlock rules clear --resource vehicle-42
|
|
|
155
155
|
| `minNoticeMinutes` | starts at least this far ahead |
|
|
156
156
|
| `maxAdvanceDays` | starts at most this far ahead |
|
|
157
157
|
| `slotStepMinutes` | starts on a step from local midnight in the resource's time zone |
|
|
158
|
+
| `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 |
|
|
159
|
+
| `bufferBeforeMinutes` | keeps this long free before it: its leading buffer, which the database enforces |
|
|
158
160
|
| `turnaroundMinutes` | keeps this long free after it: its trailing buffer, which the database enforces |
|
|
159
161
|
| `enforceHours` | lies inside the resource's bookable hours |
|
|
160
162
|
| `checkInMinutes` | is claimed (`checkInReservation`) this long after its start, or maintenance releases it |
|
|
@@ -169,8 +171,22 @@ slotlock rules clear --resource vehicle-42
|
|
|
169
171
|
step and hours. Imported calendar events are not checked: they happened elsewhere.
|
|
170
172
|
- Searches (`slotlock_find_next_available`, `slotlock_find_available_slots`, `findNextAvailableFor`)
|
|
171
173
|
offer only slots the rules allow, so an offered slot is one the write accepts.
|
|
174
|
+
- Two bookings stay at least the earlier one's trailing buffer plus the later one's leading buffer
|
|
175
|
+
apart: each occupies from its start less its leading buffer to its end plus its trailing one, and
|
|
176
|
+
the exclusion constraint keeps those apart. Free/busy reports that time as busy.
|
|
172
177
|
- The REST application API sets them with `PUT /v1/resources/{resource_id}/booking-rules`, in
|
|
173
178
|
snake_case; the store with `setResourceBookingRules`.
|
|
179
|
+
- Defaults: rules for every resource of the tenant (`PUT /v1/booking-rules`) and for every member
|
|
180
|
+
of a pool (`PUT /v1/pools/{pool}/booking-rules`), read back with `GET /v1/booking-rules`; the
|
|
181
|
+
store's `setBookingRuleDefaults` and `getBookingRuleDefaults`. A resource's rules as they apply
|
|
182
|
+
(`effective_booking_rules`, `effectiveBookingRules`) are the tenant's defaults, overlaid by its
|
|
183
|
+
pool's, overlaid by its own: a rule set at a narrower level wins. `checkInMinutes` is a resource's
|
|
184
|
+
own only. The dashboard's resource page shows which rules a resource inherits, and from where.
|
|
185
|
+
- With no rule set anywhere, nothing changes: every rule is opt-in.
|
|
186
|
+
- A tenant has default rules for at most 1,000 pools (`pool_rules_limit_reached`, 409, for one
|
|
187
|
+
more). Merged rules are read leniently: a pool's `stepOffsetMinutes` under a resource's shorter
|
|
188
|
+
step applies modulo that step, and a field that is invalid on its own is ignored (and logged),
|
|
189
|
+
never read as refusing every booking.
|
|
174
190
|
|
|
175
191
|
### Without Docker
|
|
176
192
|
|
|
@@ -304,6 +320,59 @@ resources, so you can give each person API keys without running `slotlock key cr
|
|
|
304
320
|
own on the hours page: two windows a day, overnight or all day, or closed. One form sets them
|
|
305
321
|
for every resource at once.
|
|
306
322
|
|
|
323
|
+
#### Assistant: operate Slotlock by chat
|
|
324
|
+
|
|
325
|
+
With `SLOTLOCK_ASSISTANT_SECRET` set (32+ random characters, `openssl rand -hex 32`), the dashboard
|
|
326
|
+
gains an Assistant page where every member chats with a model their organization connects, instead
|
|
327
|
+
of clicking. Each organization brings its own model:
|
|
328
|
+
|
|
329
|
+
- **Connect it** in Settings, Assistant (owners only; with personal tenants, each person): the
|
|
330
|
+
provider (`Anthropic Messages API`, or `OpenAI-compatible`, which covers OpenAI, OpenRouter,
|
|
331
|
+
vLLM, Ollama and LM Studio), the base URL (`https://api.anthropic.com`, or the one ending in
|
|
332
|
+
`/v1`: `https://api.openai.com/v1`, `http://localhost:11434/v1`; https, or http only on
|
|
333
|
+
`localhost` or `127.0.0.1`), the model name (`your-model`) and the API key, which a local model
|
|
334
|
+
may not need. As with webhooks, a URL whose host is, or resolves to, a loopback, private,
|
|
335
|
+
link-local, shared (CGNAT), unique-local or metadata address (`169.254.169.254`) is refused, when
|
|
336
|
+
it is saved and at each connection (the address the socket connects to is checked, so a name
|
|
337
|
+
rebound to one is refused too), and the page says why. For a self-hosted model on the server's
|
|
338
|
+
own machine or network (Ollama, vLLM), the operator sets `SLOTLOCK_ASSISTANT_PRIVATE_URLS=on`.
|
|
339
|
+
On Cloudflare Workers the literal check applies and names are not resolved: a Worker's fetch
|
|
340
|
+
reaches no private network. Changing the provider or the base URL takes the key again (decided in the
|
|
341
|
+
database write, so racing saves cannot get past it), so the stored key is only ever sent where it
|
|
342
|
+
was entered for, even on an origin that serves several accounts. **Test connection** makes one real request
|
|
343
|
+
and checks the model answers with one correct tool call. The organization's messages, and the bookings and resources the assistant
|
|
344
|
+
reads, are sent to the provider it configures.
|
|
345
|
+
- **The key** is stored sealed with AES-256-GCM under a key derived from `SLOTLOCK_ASSISTANT_SECRET`,
|
|
346
|
+
bound to the organization, in `slotlock.assistant_settings` (forced row-level security, erased
|
|
347
|
+
with the tenant). The dashboard shows only that it is set and its last four characters. Changing
|
|
348
|
+
the secret makes stored keys unreadable: owners enter them again.
|
|
349
|
+
- **Reads run, writes wait.** The server runs the tool loop, up to 8 model calls a message, with a
|
|
350
|
+
fixed list of the agent tools (the reads: resources, free-busy, next free time, slots, pool
|
|
351
|
+
availability, events; the writes: holds, bookings, pool and bundle bookings, changes and
|
|
352
|
+
cancellations). A read runs at once through the same operation boundary as an agent's. A write
|
|
353
|
+
never runs when the model asks: it appears as a proposed change the member approves (or
|
|
354
|
+
dismisses) with a form, and only then runs, once (its idempotency key is the dashboard's), with
|
|
355
|
+
the same booking rules, tenant limits and rate limits as an agent, recorded as
|
|
356
|
+
`dashboard-assistant:github:<user id>` (shown as "Assistant for @login"). The card leads with
|
|
357
|
+
the sentence a confirmation would ask (the resource by name, the times in its zone, and for a
|
|
358
|
+
change or cancellation the event's title, resource and time as it stands) and then shows every
|
|
359
|
+
argument in full; a change too large to show (16 KB of arguments) is not proposed, and a
|
|
360
|
+
conversation has at most 4 waiting. A proposal is only checked: the series-write slot and the
|
|
361
|
+
write's rate token are taken when the approved change runs. A message's reads share a budget of
|
|
362
|
+
16 calls and 400 resource-days. API keys, webhooks, booking rules, pools, erasure and
|
|
363
|
+
coverage are never offered; a tool the model names outside the list is refused.
|
|
364
|
+
- Tool results and booking data are data to the model, never instructions, and the system prompt
|
|
365
|
+
says so; the tool list does not change with what the model says. A member may send 10 messages
|
|
366
|
+
a minute; each model call has 30 seconds and a message 90. Conversations are kept per member in
|
|
367
|
+
`slotlock.assistant_conversations` for 2 hours after the last message, then deleted: by
|
|
368
|
+
`serve`'s maintenance, which sweeps every tenant's expired conversations every quarter hour (a
|
|
369
|
+
tenant only the dashboard serves included), by `slotlock maintain`, and whenever the tenant's
|
|
370
|
+
conversations are read (so on Workers too). They are bounded in UTF-8 bytes; **New conversation** forgets one. On Workers, the
|
|
371
|
+
assistant's agent calls are capped at 24 a request and by `tenant_read` and `tenant_write` of
|
|
372
|
+
`SLOTLOCK_LIMITS` in each isolate. A provider's failure is shown as a plain message, never with
|
|
373
|
+
the key or the provider's answer.
|
|
374
|
+
- It works as plain forms: no script is needed.
|
|
375
|
+
|
|
307
376
|
### Dashboard on Cloudflare Workers
|
|
308
377
|
|
|
309
378
|
The same dashboard can run as a Cloudflare Worker at an origin of its own (`workers/dashboard`),
|
|
@@ -337,7 +406,8 @@ this one and read its session secret, which signs in as any tenant.
|
|
|
337
406
|
3. Create the GitHub OAuth app as above, with the callback URL
|
|
338
407
|
`<SLOTLOCK_PUBLIC_URL>/dashboard/callback` on the Worker's origin.
|
|
339
408
|
4. Store the secrets on the Worker: `npx wrangler secret put` for `SLOTLOCK_GITHUB_CLIENT_ID`,
|
|
340
|
-
`SLOTLOCK_GITHUB_CLIENT_SECRET` and `SLOTLOCK_SESSION_SECRET
|
|
409
|
+
`SLOTLOCK_GITHUB_CLIENT_SECRET` and `SLOTLOCK_SESSION_SECRET`, and `SLOTLOCK_ASSISTANT_SECRET`
|
|
410
|
+
for the assistant.
|
|
341
411
|
5. Deploy: `npm ci --omit=dev` at the repository root (the Worker bundles Slotlock's sources), then
|
|
342
412
|
`npx wrangler deploy --config <your configuration>` in `workers/dashboard`.
|
|
343
413
|
|
|
@@ -780,8 +850,10 @@ import {
|
|
|
780
850
|
type SlotlockStore,
|
|
781
851
|
createSlotlockAgentServer,
|
|
782
852
|
createSlotlockStoreAgentBackend,
|
|
853
|
+
createSlotlockStoreChangeSource,
|
|
783
854
|
} from 'slotlock';
|
|
784
855
|
import { createSlotlockNodeServer } from 'slotlock/node-server';
|
|
856
|
+
import type { Sql } from 'postgres';
|
|
785
857
|
|
|
786
858
|
const ALLOWED_OPERATIONS: ReadonlySet<SlotlockAgentOperation> = new Set([
|
|
787
859
|
'slotlock_list_resources',
|
|
@@ -795,6 +867,8 @@ const ALLOWED_OPERATIONS: ReadonlySet<SlotlockAgentOperation> = new Set([
|
|
|
795
867
|
|
|
796
868
|
export interface CalendarServerConfig {
|
|
797
869
|
store: SlotlockStore;
|
|
870
|
+
/** The store's client: live updates read the change feed's head with it. */
|
|
871
|
+
sql: Sql;
|
|
798
872
|
publicBaseUrl: string;
|
|
799
873
|
port: number;
|
|
800
874
|
verifyToken(token: string): Promise<SlotlockAgentPrincipal | null>;
|
|
@@ -815,7 +889,8 @@ export async function startCalendarServer(config: CalendarServerConfig) {
|
|
|
815
889
|
}
|
|
816
890
|
: {}),
|
|
817
891
|
resourceWindowDays: 90,
|
|
818
|
-
|
|
892
|
+
// Live updates follow each tenant's change feed: a booking is announced as it commits.
|
|
893
|
+
subscriptions: { changes: createSlotlockStoreChangeSource(config.store, config.sql) },
|
|
819
894
|
backend: createSlotlockStoreAgentBackend(config.store, {
|
|
820
895
|
availabilityRules: async () => [
|
|
821
896
|
{ rrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', startMinutes: 9 * 60, durationMinutes: 480 },
|
|
@@ -903,7 +978,9 @@ Rentals sell a class, not a plate. Put interchangeable resources in a pool
|
|
|
903
978
|
members with `slotlock_list_resources` and `pool`, and book the class with `slotlock_book_pool`,
|
|
904
979
|
which takes the first member that is free and allows the booking, in one transaction: a class of
|
|
905
980
|
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
|
|
981
|
+
free, `slotlock_get_pool_availability` splits a window where that number changes (counting each
|
|
982
|
+
member busy over its bookings' occupied time, buffers included, but not the buffers a new booking
|
|
983
|
+
would keep, so a member it counts free may still refuse a booking that abuts another); to see which,
|
|
907
984
|
search the members with `slotlock_find_available_slots` and `require_free: 1`: each slot names the
|
|
908
985
|
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
|
|
909
986
|
are no longer advertised.
|
|
@@ -944,7 +1021,7 @@ on stderr.
|
|
|
944
1021
|
| HTTP | `401` with `WWW-Authenticate`, `403` disallowed origin, `413`/`415` bad body, `429` rate limit (MCP, REST) |
|
|
945
1022
|
| MCP | `-32602` unknown tool, `arguments` that are not an object, or altered confirmation; `-32603` server fault; `-32601` unknown method |
|
|
946
1023
|
| MCP 2026-07-28 | `-32020` header disagrees with body, `-32021` form elicitation not declared, `-32022` unsupported revision, `-31029` rate limit |
|
|
947
|
-
| 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`
|
|
1024
|
+
| 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) |
|
|
948
1025
|
|
|
949
1026
|
### Authentication and OAuth discovery
|
|
950
1027
|
|
|
@@ -995,8 +1072,18 @@ On MCP 2026-07-28 each resource is also an MCP resource, `slotlock://resources/{
|
|
|
995
1072
|
returns busy time and coverage for the next `resourceWindowDays` (30). `subscriptions/listen` streams
|
|
996
1073
|
`notifications/resources/updated` when a resource's free/busy changes.
|
|
997
1074
|
|
|
998
|
-
-
|
|
999
|
-
|
|
1075
|
+
- With `subscriptions.changes` (`createSlotlockStoreChangeSource(store, sql)`; `slotlock serve`
|
|
1076
|
+
sets it), changes come from the tenant's change feed: one reader per tenant per process, shared by
|
|
1077
|
+
all its subscriptions, polls the feed by cursor, one indexed query per read, under the tenant's
|
|
1078
|
+
row-level security. It reads again 250 ms after a read that found changes
|
|
1079
|
+
(`SLOTLOCK_FEED_BUSY_MS`, also the least gap between two reads) and backs off to once a second
|
|
1080
|
+
when quiet (`SLOTLOCK_FEED_IDLE_MS`), so a booking is announced within about a second. A change
|
|
1081
|
+
re-reads the subscriptions watching its resource, at most once a second each however many
|
|
1082
|
+
changes arrive, and each announces only what actually changed in its window. A cursor the feed
|
|
1083
|
+
has pruned past announces every watched resource once, then follows from now.
|
|
1084
|
+
- Each subscription is also re-read every `pollIntervalMs` (15 s with a feed, 10 s without one,
|
|
1085
|
+
which is then the only way changes are found), re-checking the caller each time, so a revoked key
|
|
1086
|
+
or what the feed does not carry (bookable hours) is caught.
|
|
1000
1087
|
- A subscription ends after `maxDurationMs` (15 minutes), on `shutdown()`, or when access is lost.
|
|
1001
1088
|
- At most 4 open per caller and 256 per server (`maxPerPrincipal`, `maxTotal`). `maxPerTenant`
|
|
1002
1089
|
caps one tenant's across all its callers (default `maxTotal`; `slotlock serve` sets 16), so a
|
|
@@ -1154,12 +1241,37 @@ curl -X PUT "http://localhost:8080/v1/reservations/BK-42" \
|
|
|
1154
1241
|
|
|
1155
1242
|
### Changes and webhooks
|
|
1156
1243
|
|
|
1157
|
-
Every committed change to a tenant's holds, bookings
|
|
1158
|
-
transaction (by a database trigger, so no write path skips it), and nothing is recorded
|
|
1159
|
-
not commit. A change carries ids, times and states, never titles, descriptions
|
|
1244
|
+
Every committed change to a tenant's holds, bookings, calendar events and configuration is recorded
|
|
1245
|
+
in the same transaction (by a database trigger, so no write path skips it), and nothing is recorded
|
|
1246
|
+
that did not commit. A change carries ids, times and states, never titles, descriptions, attendees,
|
|
1247
|
+
webhook URLs or secrets.
|
|
1248
|
+
|
|
1249
|
+
- The types: `hold.created`, `.updated`, `.confirmed`, `.released`, `.expired`;
|
|
1250
|
+
`reservation.created`, `.updated`, `.cancelled`, `.checked_in`, `.no_show`; `event.created`,
|
|
1251
|
+
`.updated`, `.cancelled`, `.timezone_shifted`, `.timezone_conflict`; `waitlist.offered`,
|
|
1252
|
+
`.expired`; `coverage.stale`; and configuration: `resource.created`, `resource.updated` (booking
|
|
1253
|
+
and check-in rules, pool, bookable hours, time zone or reference, with `before` and `after` of
|
|
1254
|
+
only the fields that changed), `resource.archived`, `.restored`, `.deleted`; `coverage.recorded`
|
|
1255
|
+
(a source's first read; reading it again is not a change) and `coverage.deleted`. A `coverage.*`
|
|
1256
|
+
change names its source by `source_id` (also its `subject`), a digest of the tenant and the source
|
|
1257
|
+
that `slotlock_get_coverage` and `slotlockCoverageSourceId` give too, never the source itself,
|
|
1258
|
+
which can be an iCalendar URL with a token or an address;
|
|
1259
|
+
`webhook.created`, `.enabled`, `.disabled`, `.deleted` (the id and `event_types`, never the URL);
|
|
1260
|
+
`key.created`, `.rotated`, `.revoked` (the id, scopes and expiry, never the key, its digest,
|
|
1261
|
+
prefix or name). `SLOTLOCK_CHANGE_TYPES` lists them.
|
|
1262
|
+
- Each change records who made it (`actor`: the API key or agent), the API key's id (`key_id`) when
|
|
1263
|
+
a key made it, and where it came from (`origin`): `mcp`, `a2a`, `rest` (the REST agent routes),
|
|
1264
|
+
`application` (the application API), `dashboard`, `dashboard-assistant`, `maintenance`, or
|
|
1265
|
+
`library`: code calling the store itself, inside `withTenant` with no `origin` or outside it, and
|
|
1266
|
+
any write that names none. Never an address or anything else about a person. A change an earlier
|
|
1267
|
+
release recorded has neither (`null`).
|
|
1268
|
+
- `key_id` is pseudonymous: the key's id, never the key or its name. It stays on the changes the key
|
|
1269
|
+
made after the key is revoked or deleted, for as long as the feed keeps them (the retention below,
|
|
1270
|
+
and longer while an enabled webhook has still to receive them); erasing the tenant removes them.
|
|
1160
1271
|
|
|
1161
1272
|
- `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`, …)
|
|
1273
|
+
(`io.pylota.slotlock.hold.created`, `.reservation.cancelled`, `.event.updated`, …), with the
|
|
1274
|
+
extension attributes `slotlockorigin` and `slotlockkeyid` when the change records them. Follow
|
|
1163
1275
|
`next_cursor`, which comes back even when nothing is new: it never skips a change, even one whose
|
|
1164
1276
|
transaction commits after a later one's. Changes are kept 30 days (`SLOTLOCK_RETENTION_DAYS`,
|
|
1165
1277
|
`retentionDays` in `createSlotlockStore`, 1-3,650, for `serve` and `maintain` alike); a cursor
|
|
@@ -1170,7 +1282,7 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1170
1282
|
- `POST /v1/webhooks` with an https `url` (and `event_types`, optionally) sends each change from
|
|
1171
1283
|
then on as a [Standard Webhooks](https://www.standardwebhooks.com) request: `webhook-id` (the
|
|
1172
1284
|
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
|
|
1285
|
+
`whsec_` secret the call returns once. `event_types` takes any of the types above. Verify it with any Standard Webhooks library, and use
|
|
1174
1286
|
`webhook-id` to drop a repeat: delivery is at least once.
|
|
1175
1287
|
- Changes go to each webhook in order, one at a time, until one fails; that one is retried after
|
|
1176
1288
|
5 seconds, 5 minutes, 30 minutes, 2, 5, then every 10 hours. After a day of failures, or a `410`,
|
|
@@ -1180,11 +1292,14 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1180
1292
|
an enabled webhook has still to receive, however old. At most 10 webhooks per tenant.
|
|
1181
1293
|
- Each request has 15 seconds in all, and a tenant's pass 30 seconds; tenants are delivered 8 at
|
|
1182
1294
|
a time, so a slow endpoint holds up only its own tenant.
|
|
1183
|
-
- History reads the same record backwards, with who made each change (
|
|
1184
|
-
for a change of an existing booking or
|
|
1185
|
-
`GET /v1/resources/{id}/history
|
|
1186
|
-
`GET /v1/tenant/events/{id}/history`, newest first,
|
|
1187
|
-
|
|
1295
|
+
- History reads the same record backwards, with who made each change (`actor`, `key_id`), where
|
|
1296
|
+
from (`origin`) and, for a change of an existing booking, event or resource, what it was before:
|
|
1297
|
+
`GET /v1/resources/{id}/history` (its bookings, events and its own configuration),
|
|
1298
|
+
`GET /v1/reservations/{reference}/history`, `GET /v1/tenant/events/{id}/history`, newest first,
|
|
1299
|
+
paged by `before`. In the library, pass `actor`, `origin` and `keyId` to `withTenant` (`origin`
|
|
1300
|
+
defaults to the enclosing call's, else `library`; `createSlotlockApiKeyAuthenticator` gives the
|
|
1301
|
+
principal its `keyId`), and `actor` and `origin` to the API key store's `create`, `rotate` and
|
|
1302
|
+
`revoke`; read `listHistory`. It is kept as long as the feed and erased with
|
|
1188
1303
|
the tenant.
|
|
1189
1304
|
- When time zone rules change (a new tzdata), a booking's instant and the wall time it was made for
|
|
1190
1305
|
part: Winnipeg's 09:00 is no longer the instant stored. Slotlock keeps both, and maintenance finds
|
|
@@ -1193,7 +1308,8 @@ not commit. A change carries ids, times and states, never titles, descriptions o
|
|
|
1193
1308
|
to `maintainSlotlockTenant` moves them instead, as a new revision, unless that overlaps another
|
|
1194
1309
|
booking (`event.timezone_conflict`; it stays, for a person or an agent to decide). `/healthz`
|
|
1195
1310
|
reports the `tzdata` the server runs.
|
|
1196
|
-
- Feed health: `GET /v1/resources/{id}/coverage` shows each source
|
|
1311
|
+
- Feed health: `GET /v1/resources/{id}/coverage` shows each source (and its `source_id`, as the feed
|
|
1312
|
+
names it), its window, when it was last read,
|
|
1197
1313
|
its last error (`POST /v1/resources/{id}/coverage/{source}/failure` records one) and whether it is
|
|
1198
1314
|
stale (not read for a day, so the time it covers is unproven, never free). A source that goes stale
|
|
1199
1315
|
is put on the feed once, as `coverage.stale`, until it is read again. Retire a sync you have
|
|
@@ -1367,6 +1483,15 @@ export async function listResourcesOverA2a(baseUrl: string, token: string) {
|
|
|
1367
1483
|
Send one `application/json` data part, `{"skill": "<id>", "arguments": {…}}`, with header
|
|
1368
1484
|
`A2A-Version: 1.0`. Each skill in the agent card has an example; replies are synchronous.
|
|
1369
1485
|
|
|
1486
|
+
The card declares `streaming`. `SendStreamingMessage` with any skill streams its reply as one
|
|
1487
|
+
message. With `slotlock_watch_resources` (`{"resource_ids": [...]}`, up to 20) it streams a watch:
|
|
1488
|
+
a task in `TASK_STATE_WORKING` naming the resources it can see, then a status update
|
|
1489
|
+
`{"resource_id", "uri"}` each time one's free/busy changes, from the same feed and under the same
|
|
1490
|
+
caps as MCP subscriptions, until `TASK_STATE_COMPLETED` (its duration, shutdown) or
|
|
1491
|
+
`TASK_STATE_FAILED` (access withdrawn). Closing the stream ends it; the task is not kept, so
|
|
1492
|
+
`SubscribeToTask` and `GetTask` do not find it. Push notifications are not offered: a tenant's
|
|
1493
|
+
changes reach a URL as signed webhooks.
|
|
1494
|
+
|
|
1370
1495
|
### MCP Apps
|
|
1371
1496
|
|
|
1372
1497
|
Read-only tools link to `ui://slotlock/calendar`, a self-contained calendar view that hosts
|
|
@@ -1452,7 +1577,9 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1452
1577
|
7. Monitor conflicts, refused rules, sync lag, capped maintenance runs, quota errors and confirmation outcomes.
|
|
1453
1578
|
8. Keep writes confirmation-gated wherever your policy needs a person (`slotlock serve`:
|
|
1454
1579
|
`SLOTLOCK_CONFIRM_WRITES`, `none` by default).
|
|
1455
|
-
9. Size live updates:
|
|
1580
|
+
9. Size live updates: one indexed feed query per second per tenant with an open subscription (four
|
|
1581
|
+
a second while its feed is busy), at most one backend read a second per subscription a change
|
|
1582
|
+
touches, and at most `maxTotal` backend reads per `pollIntervalMs`.
|
|
1456
1583
|
10. Give each agent its own API key with the narrowest scope and an expiry.
|
|
1457
1584
|
11. After each deployment, check it end to end with a short-lived admin key, then revoke the key:
|
|
1458
1585
|
`SLOTLOCK_URL=https://… SLOTLOCK_ADMIN_KEY=… node scripts/smoke-deployment.mjs`, from a clone
|
|
@@ -1463,7 +1590,13 @@ export async function maintainTenant(store: SlotlockStore, tenantRef: string) {
|
|
|
1463
1590
|
server (`SLOTLOCK_LIMITS`); behind a proxy, set `SLOTLOCK_TRUSTED_PROXY_HEADER` and firewall
|
|
1464
1591
|
the server so nothing reaches it except through that proxy. Embedding the
|
|
1465
1592
|
agent server, give it a `consumeRateLimit`. Watch serve's `refusals` log line for floods.
|
|
1466
|
-
13.
|
|
1593
|
+
13. On an upgrade, migrate first, then, as a library user, run `grantApplicationRole` again right
|
|
1594
|
+
after `applySchema` and before serving traffic: a release that adds a table (the leading-buffer
|
|
1595
|
+
release adds `booking_rule_defaults`, which every booking reads) fails every booking with a
|
|
1596
|
+
permission error until the serving role is granted it. `slotlock migrate` does both. A migration
|
|
1597
|
+
that rebuilds a constraint holds its table's lock for the whole build (see the CHANGELOG's
|
|
1598
|
+
Upgrading notes).
|
|
1599
|
+
14. Bound how long a transaction may run. A store over your own transaction (a savepoint per call)
|
|
1467
1600
|
keeps its locks and its snapshot for as long as yours runs, and a hold you confirm inside it is
|
|
1468
1601
|
judged by the statement's clock, not the transaction's. On PostgreSQL 17 and later, give the
|
|
1469
1602
|
serving role a `transaction_timeout` above your longest legitimate transaction
|
package/SPEC.md
CHANGED
|
@@ -245,13 +245,16 @@ envelope (a body over the size limit is HTTP 413); A2A's own errors, below, carr
|
|
|
245
245
|
`google.rpc.ErrorInfo` detail and JSON-RPC's (`-32700`, `-32600`, `-32601`, `-32603`) none. An
|
|
246
246
|
`A2A-Version` other than `1.0`, including none (read as 0.3), is `VersionNotSupportedError`
|
|
247
247
|
(`-32009`); push-notification methods are
|
|
248
|
-
`PushNotificationNotSupportedError` (`-32003`);
|
|
249
|
-
|
|
248
|
+
`PushNotificationNotSupportedError` (`-32003`); task subscription and the extended card are
|
|
249
|
+
`UnsupportedOperationError` (`-32004`); a message that is not exactly one data part
|
|
250
250
|
`{"skill": "<id>", "arguments": {…}}` (`arguments` left out or null is none; any other value that is
|
|
251
251
|
not an object is not arguments) is `-32602`, as is a `configuration` or `metadata` that is set but
|
|
252
252
|
is not an object; a rate-limited request is `-32029` with the reason `RATE_LIMITED` and a null id,
|
|
253
|
-
answered before its body is read.
|
|
254
|
-
|
|
253
|
+
answered before its body is read. `SendStreamingMessage` streams a skill's reply as one message;
|
|
254
|
+
with `slotlock_watch_resources` it streams a watch task (TASK_STATE_WORKING, a status update per
|
|
255
|
+
changed resource, then TASK_STATE_COMPLETED or TASK_STATE_FAILED) that lives only in its stream,
|
|
256
|
+
under the MCP subscription caps; refused before it opens, it is the skill's reply. The server keeps
|
|
257
|
+
no tasks, so the core task operations answer as for an unknown task: `GetTask` and `CancelTask` are
|
|
255
258
|
`TaskNotFoundError` (`-32001`), `ListTasks` returns an empty page (`pageSize` 1-100, default 50).
|
|
256
259
|
Their params are checked first, as `SendMessage`'s are: a field the method does not define, a
|
|
257
260
|
`tenant` other than the caller's, a value of the wrong type (a `status` outside `TaskState`, say), a
|
|
@@ -364,9 +367,9 @@ with no remaining command; `internal` tombstones remain authoritative until tena
|
|
|
364
367
|
command evaluated after its evidence is gone is decided afresh by the expected-revision guard.
|
|
365
368
|
|
|
366
369
|
The protocol backend remains injectable so a deployment owns credential verification, policy, rate
|
|
367
|
-
limits, and networking. The A2A binding
|
|
368
|
-
|
|
369
|
-
persists that lifecycle itself.
|
|
370
|
+
limits, and networking. The A2A binding advertises streaming (a reply as one message, a watch as
|
|
371
|
+
a task that lives only in its stream) and not push notifications; an embedder must not claim
|
|
372
|
+
persisted task lifecycle support unless it persists that lifecycle itself.
|
|
370
373
|
|
|
371
374
|
The optional Node HTTP adapter preserves the Fetch boundary rather than defining another protocol
|
|
372
375
|
implementation. It constructs callback-visible request URLs from an explicit canonical origin,
|
package/agent-server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type SlotlockTraceContext } from './mcp-modern.js';
|
|
2
2
|
import { type SlotlockApplicationBackend, type SlotlockApplicationOperation } from './application-api.js';
|
|
3
|
+
import { type SlotlockAgentChangeSource } from './change-watch.js';
|
|
4
|
+
import type { SlotlockChangeOrigin } from './event-feed.js';
|
|
3
5
|
export type { SlotlockTraceContext } from './mcp-modern.js';
|
|
4
6
|
export { clientSupportsSlotlockMcpApp, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, } from './mcp-app.js';
|
|
5
7
|
/**
|
|
@@ -64,6 +66,11 @@ export interface SlotlockAgentPrincipal {
|
|
|
64
66
|
* Unset, `authorize` alone decides.
|
|
65
67
|
*/
|
|
66
68
|
scopes?: readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* The API key the credential is, by its id (a UUID), when it is one: the history records it with
|
|
71
|
+
* each change the principal makes (`createSlotlockApiKeyAuthenticator` sets it).
|
|
72
|
+
*/
|
|
73
|
+
keyId?: string;
|
|
67
74
|
}
|
|
68
75
|
export interface SlotlockAgentInvocationContext {
|
|
69
76
|
principal: SlotlockAgentPrincipal;
|
|
@@ -74,6 +81,11 @@ export interface SlotlockAgentInvocationContext {
|
|
|
74
81
|
* backend's spans as its children to follow one agent action across services.
|
|
75
82
|
*/
|
|
76
83
|
trace?: SlotlockTraceContext;
|
|
84
|
+
/**
|
|
85
|
+
* The surface the call came through (`mcp`, `a2a`, `rest`, `dashboard-assistant`), for a backend
|
|
86
|
+
* to record with its changes (withTenant's `origin`).
|
|
87
|
+
*/
|
|
88
|
+
origin?: SlotlockChangeOrigin;
|
|
77
89
|
}
|
|
78
90
|
/** A bounded domain failure a backend may deliberately expose through a protocol tool result. */
|
|
79
91
|
export declare class SlotlockAgentOperationError extends Error {
|
|
@@ -182,13 +194,27 @@ export interface SlotlockAgentConfirmationOptions {
|
|
|
182
194
|
/**
|
|
183
195
|
* Live calendar updates over MCP 2026-07-28 `subscriptions/listen`. A client subscribes to calendar
|
|
184
196
|
* resource URIs (`slotlockCalendarResourceUri`) and receives `notifications/resources/updated` on the
|
|
185
|
-
* response stream when a resource's free/busy changes.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
197
|
+
* response stream when a resource's free/busy changes. With `changes`, the server follows each
|
|
198
|
+
* tenant's change feed (one reader per tenant, shared by all its subscriptions) and re-reads a
|
|
199
|
+
* subscription's resources when a change touches one of them; without it, it re-reads them every
|
|
200
|
+
* `pollIntervalMs`. Each re-read runs `authenticate` and `authorize` again, so a revoked credential
|
|
201
|
+
* or permission ends the stream, and announces only the resources whose free/busy changed.
|
|
188
202
|
*/
|
|
189
203
|
export interface SlotlockAgentSubscriptionOptions {
|
|
190
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* How often an open subscription re-reads its resources regardless, in ms: 1,000-300,000. Default
|
|
206
|
+
* 10,000, or 15,000 with `changes`, where it is the fallback for what the feed does not carry
|
|
207
|
+
* (bookable hours, a revoked key or permission) and for a feed that stopped.
|
|
208
|
+
*/
|
|
191
209
|
pollIntervalMs?: number;
|
|
210
|
+
/**
|
|
211
|
+
* The change feed to follow (createSlotlockStoreChangeSource, or your own over
|
|
212
|
+
* SlotlockEventFeed.listChanges), polled by one reader per tenant every SLOTLOCK_FEED_BUSY_MS to
|
|
213
|
+
* SLOTLOCK_FEED_IDLE_MS: a change is announced within about a second instead of at the next
|
|
214
|
+
* re-read. A subscription re-reads its resources at most once a second, however many changes
|
|
215
|
+
* touch them.
|
|
216
|
+
*/
|
|
217
|
+
changes?: SlotlockAgentChangeSource;
|
|
192
218
|
/** When the server ends a subscription gracefully, in ms: 60,000-86,400,000, default 900,000. */
|
|
193
219
|
maxDurationMs?: number;
|
|
194
220
|
/** How often a quiet stream carries an SSE keep-alive comment, in ms: 1,000-60,000, default 15,000. */
|
|
@@ -449,6 +475,8 @@ export declare function invokeSlotlockAgentOperation(args: {
|
|
|
449
475
|
options: SlotlockAgentOperationDispatchOptions;
|
|
450
476
|
/** Validated W3C trace context to hand the backend (see `SlotlockAgentInvocationContext.trace`). */
|
|
451
477
|
trace?: SlotlockTraceContext;
|
|
478
|
+
/** The surface the call came through, handed to the backend (`SlotlockAgentInvocationContext.origin`). */
|
|
479
|
+
origin?: SlotlockChangeOrigin;
|
|
452
480
|
}): Promise<SlotlockAgentOperationDispatchResult>;
|
|
453
481
|
/**
|
|
454
482
|
* Read an A2A `google.protobuf.Timestamp` in its JSON form, as `ListTasks.statusTimestampAfter`
|
|
@@ -464,6 +492,26 @@ export declare function parseSlotlockA2ATimestamp(value: unknown): Date | null;
|
|
|
464
492
|
export declare const SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE = "slotlock://resources/{resource_id}";
|
|
465
493
|
/** The MCP resource URI of one calendar resource: `SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE`, expanded. */
|
|
466
494
|
export declare function slotlockCalendarResourceUri(resourceId: string): string;
|
|
495
|
+
/**
|
|
496
|
+
* The sentence a person is asked to confirm for a write, as the confirmation flow words it: the
|
|
497
|
+
* resource by name and the times in its own zone, and for a change or deletion the event as it
|
|
498
|
+
* stands (its title, resource and time), each read as `principal` only where its scopes and
|
|
499
|
+
* `authorize` allow that read. The dashboard assistant shows it on a proposed change.
|
|
500
|
+
*/
|
|
501
|
+
export declare function describeSlotlockAgentWrite(args: {
|
|
502
|
+
backend: SlotlockAgentCalendarBackend;
|
|
503
|
+
authorize: SlotlockAgentServerOptions['authorize'];
|
|
504
|
+
principal: SlotlockAgentPrincipal;
|
|
505
|
+
operation: SlotlockAgentWriteOperation;
|
|
506
|
+
input: Record<string, unknown>;
|
|
507
|
+
signal: AbortSignal;
|
|
508
|
+
}): Promise<string>;
|
|
509
|
+
/**
|
|
510
|
+
* The A2A skill that watches calendars: `SendStreamingMessage` only. Its stream opens with a task
|
|
511
|
+
* in TASK_STATE_WORKING and carries a status update for each change to a watched resource's
|
|
512
|
+
* free/busy, from the same feed as MCP resource subscriptions, until the watch's duration is up.
|
|
513
|
+
*/
|
|
514
|
+
export declare const SLOTLOCK_A2A_WATCH_SKILL = "slotlock_watch_resources";
|
|
467
515
|
export declare function createSlotlockAgentServer(options: SlotlockAgentServerOptions): {
|
|
468
516
|
fetch(request: Request): Promise<Response>;
|
|
469
517
|
manifest: Readonly<Record<string, unknown>>;
|