slotlock 0.1.1 → 0.3.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/CONTRIBUTING.md +55 -19
  3. package/GOVERNANCE.md +4 -2
  4. package/MAINTAINERS.md +3 -2
  5. package/NOTICE +4 -4
  6. package/README.md +403 -103
  7. package/SECURITY.md +70 -5
  8. package/SPEC.md +161 -21
  9. package/agent-server.d.ts +76 -10
  10. package/agent-server.js +381 -146
  11. package/agent-store-backend.d.ts +22 -0
  12. package/agent-store-backend.js +598 -24
  13. package/api-keys.d.ts +22 -7
  14. package/api-keys.js +38 -8
  15. package/application-api.d.ts +67 -0
  16. package/application-api.js +552 -0
  17. package/config.d.ts +93 -0
  18. package/config.js +192 -0
  19. package/dashboard.d.ts +10 -1
  20. package/dashboard.js +23 -5
  21. package/ddl.d.ts +47 -4
  22. package/ddl.js +679 -25
  23. package/engine.js +20 -6
  24. package/event-schema.d.ts +59 -0
  25. package/event-schema.js +85 -0
  26. package/hooks.d.ts +7 -0
  27. package/hooks.js +13 -0
  28. package/ical-worker-thread.js +4 -2
  29. package/ical-worker.d.ts +8 -5
  30. package/ical-worker.js +9 -2
  31. package/index.d.ts +12 -8
  32. package/index.js +7 -5
  33. package/maintenance.d.ts +67 -0
  34. package/maintenance.js +213 -0
  35. package/node-server.d.ts +9 -1
  36. package/node-server.js +127 -6
  37. package/openapi.json +6024 -0
  38. package/package.json +3 -2
  39. package/rate-limit.d.ts +71 -0
  40. package/rate-limit.js +718 -0
  41. package/rest-api.d.ts +33 -18
  42. package/rest-api.js +141 -63
  43. package/rules.d.ts +19 -0
  44. package/rules.js +38 -7
  45. package/self-host.d.ts +26 -9
  46. package/self-host.js +427 -47
  47. package/slotlock-manifest.schema.json +6 -1
  48. package/slotlock.manifest.json +7 -2
  49. package/store.d.ts +359 -27
  50. package/store.js +2077 -296
  51. package/sync.d.ts +43 -2
  52. package/sync.js +1950 -277
  53. package/timezone.d.ts +77 -3
  54. package/timezone.js +265 -21
  55. package/types.d.ts +217 -10
  56. package/weekly-hours.js +2 -2
package/SECURITY.md CHANGED
@@ -7,9 +7,10 @@ documented breaking changes; security releases will identify the lowest safe ver
7
7
 
8
8
  ## Report a vulnerability
9
9
 
10
- Do not open a public issue. Report privately through GitHub's private vulnerability reporting on the
11
- repository that publishes the package: open its **Security** tab and choose **Report a
12
- vulnerability**. The maintainers listed in [MAINTAINERS.md](./MAINTAINERS.md) receive the report.
10
+ Do not open a public issue. Report privately through GitHub's private vulnerability reporting:
11
+ <https://github.com/PILOTAAI/slotlock/security/advisories/new> (the repository's **Security** tab,
12
+ **Report a vulnerability**). The maintainers listed in
13
+ [MAINTAINERS.md](https://github.com/PILOTAAI/slotlock/blob/main/MAINTAINERS.md) receive the report.
13
14
  Include:
14
15
 
15
16
  - the affected version and surface;
@@ -18,7 +19,22 @@ Include:
18
19
  - any known mitigations.
19
20
 
20
21
  Do not include real customer, credential, calendar, or booking data. We will acknowledge a report
21
- within three business days and coordinate disclosure after a fix is available.
22
+ within three business days and coordinate disclosure after a fix is available. Fixed vulnerabilities
23
+ are published as advisories at <https://github.com/PILOTAAI/slotlock/security/advisories>.
24
+
25
+ ## Testing hosted Slotlock
26
+
27
+ Hosted Slotlock (`slotlock-api.pylota.io` and `app.slotlock.pylota.io`, run by TREFT LTD) is in scope,
28
+ under these rules, which its [Terms of Service](TERMS.md) and
29
+ [Acceptable Use Policy](ACCEPTABLE-USE.md) refer to:
30
+
31
+ - Test only against an account of your own, signed in with your own GitHub account.
32
+ - Never read, change or keep another account's data. If a test reaches any, stop and report it at
33
+ once, and delete what you saw.
34
+ - Run no denial of service, load test or automated scanning that sends more than the published
35
+ limits ([hosted service page](docs/hosted/README.md#limits)); run Slotlock yourself for that.
36
+ - Report through private vulnerability reporting as above, and give us the time to fix it before
37
+ you disclose anything.
22
38
 
23
39
  ## Security model
24
40
 
@@ -28,6 +44,16 @@ PostgreSQL exclusion constraint. Tenant policies are forced and fail closed when
28
44
  context is missing. Embedding applications remain responsible for authentication, authorization,
29
45
  provider-secret storage, webhook verification, network egress, and erasure.
30
46
 
47
+ Upgrading repairs data across tenants (expired holds in a turnaround buffer, duplicate resources,
48
+ archives and event details from earlier versions), which forced policies would hide from the owning
49
+ role. `applySchema` therefore lifts FORCE from the tables that have it for its data steps and puts
50
+ it back on exactly those, inside its one locked transaction: a failure rolls the lift back with the
51
+ rest. Run statement by statement instead (psql without `--single-transaction`), a refused step
52
+ stops the run with FORCE lifted. The tables lifted are recorded in
53
+ `slotlock.deployment_lifted_row_security`, so the next deployment, from any session, puts FORCE back
54
+ on each of them, and until it does `slotlock serve` refuses to start (`row_security_lifted`). Run
55
+ the deployment DDL through `applySchema` or `slotlock migrate`.
56
+
31
57
  Application traffic must use a role that cannot leave forced RLS. `grantApplicationRole` grants
32
58
  such a role schema usage and DML on the tenant tables, and first refuses the known ways out, whether
33
59
  the role holds them itself, through a role it belongs to, or through `PUBLIC`, and names the ones
@@ -96,4 +122,43 @@ protected-resource metadata, when configured, only tells clients where to obtain
96
122
  embedder's `authenticate` must verify every token, including its audience. Embedders must provide
97
123
  bearer-token verification, per-principal authorization, distributed rate limiting, TLS termination,
98
124
  and a readiness check. Do not place provider credentials or raw calendar content in authentication,
99
- authorization, rate-limit, or health callbacks.
125
+ authorization, rate-limit, or health callbacks. `slotlock serve`, its own embedder, limits requests
126
+ per client address before any credential is looked up, then per key, per tenant and for the whole
127
+ server, caps writes that expand a recurring series, maintenance runs and live-update streams per
128
+ tenant, answers a full key-lookup queue as busy rather than unauthenticated, and logs counts of
129
+ refusals with each client as a pseudonym (`SLOTLOCK_LIMITS`, `SLOTLOCK_TRUSTED_PROXY_HEADER`). A
130
+ key that authenticated in the last 15 minutes waits in a lookup queue of its own, served first; a
131
+ valid key that has not authenticated in the last 15 minutes (any key, after a restart) waits with
132
+ made-up keys and may be answered busy (503 with `Retry-After`) while a flood of them lasts, never
133
+ 401. Its counts are one process's: several instances each limit alone.
134
+
135
+ Write confirmation (MCP 2026-07-28 form elicitation) seals the pending write to the caller, the tool
136
+ and its exact arguments, but the answer comes from the client: Slotlock relies on the MCP client to
137
+ ask its person, and a key holder's own client can answer yes. It keeps an agent from writing unasked;
138
+ it is not an authorization boundary. Scopes and `authorize` decide who may write.
139
+
140
+ ## API keys and the application API
141
+
142
+ An API key is `slk_`, 40 base62 characters from the operating system's random source (about 238
143
+ bits) and a 6-character checksum, and the database keeps only its SHA-256. A slow password hash
144
+ (bcrypt, scrypt, Argon2) defends a secret a person chose against offline guessing; nobody can guess
145
+ 238 random bits, so a fast hash
146
+ loses nothing and lets every request look a key up by its digest in one indexed read. Code scanners
147
+ that treat every secret as a password report this as weak hashing (CodeQL
148
+ `js/insufficient-password-hash`); it is the design, as for GitHub's and Stripe's tokens. The
149
+ repository's CodeQL configuration (`.github/codeql/codeql-config.yml`) drops that one query with
150
+ this reason beside it, rather than dismissing its alerts one by one.
151
+
152
+ A key acts only in the tenant it was created for. `read` and `write` cover the agent tools; `admin`
153
+ covers the application API (resources, hours, holds, reservations, coverage, maintenance), which is
154
+ never offered to agents over MCP or A2A. Every application operation checks the tenant itself, not
155
+ only through row-level security: another tenant's resource or hold is not found even on a connection
156
+ that bypasses RLS.
157
+
158
+ ## Dependencies
159
+
160
+ The published package depends on `ical.js`, `rrule` and `zod`, and on `postgres` as a peer; the
161
+ image adds nothing else. The documentation site (`site/`) is built with Astro and Starlight, and
162
+ those development dependencies never reach the package or the image. `braces`, which the site's
163
+ `starlight-llms-txt` reaches through `micromatch`, has an open denial-of-service advisory with no fixed
164
+ release; it only expands the site's own glob patterns at build time, so no outside input reaches it.
package/SPEC.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Slotlock agent-calendar specification 1.0
2
2
 
3
- This document defines the interoperable scheduling semantics of Slotlock 0.1. Implementations may use
4
- different transports or persistence layers, but must preserve these rules when claiming compatibility.
3
+ This document defines the interoperable scheduling semantics of Slotlock 0.2 and later.
4
+ Implementations may use different transports or persistence layers, but must preserve these rules
5
+ when claiming compatibility. Which of them may change between releases is in VERSIONING.md.
5
6
 
6
7
  ## Resource model
7
8
 
@@ -15,8 +16,71 @@ rule assumes a particular industry.
15
16
  - Availability rules use local wall time in the resource's IANA timezone.
16
17
  - Every interval is half-open `[start, end)`. `end` must be strictly after `start`.
17
18
  - Adjacent intervals do not conflict. Overlapping or touching availability windows may be merged.
18
- - Daylight-saving gaps move a nonexistent wall time forward by the gap. Ambiguous fall-back wall
19
- times resolve to the first occurrence.
19
+ - An event's time zone is an IANA name, kept in its canonical spelling (`europe/london` is stored
20
+ as `Europe/London`). A UTC offset such as `+05:00` is not a zone, and an event in one is refused.
21
+ - An event time in an hour the clocks skip moves forward by the gap, and one in an hour they repeat
22
+ is its first instant (RFC 5545 §3.3.5), except where "Daylight-saving time" below says otherwise.
23
+ - An availability window's edge in an hour the clocks skip stands where that hour ends, so a window
24
+ holds only the wall times it names that exist: in New York on 2027-03-14, [02:30, 03:30) is
25
+ 03:00-03:30 EDT and [02:00, 02:30) is nothing. An edge in a repeated hour is its first instant.
26
+
27
+ ## Daylight-saving time
28
+
29
+ An occurrence of a timed recurring event starts at its wall time read as above, and ends at the
30
+ later of two readings:
31
+
32
+ - its exact end: its start plus the series' exact length, DTEND − DTSTART as instants (RFC 5545
33
+ §3.8.5.3), or its DURATION, whose days and weeks are calendar days and whose hours, minutes and
34
+ seconds are exact (§3.3.6);
35
+ - its wall-clock end: DTEND's wall time as written, carried to the occurrence's date (one written
36
+ in an hour the clocks skip is carried as written), the later instant where the clocks repeat
37
+ that hour. A stored series keeps its first occurrence as instants, and reads this end from its
38
+ end instant.
39
+
40
+ It occupies `[start, later end)`, so whichever way people read an occurrence that the clocks
41
+ change inside, none of the time they hold is offered as free. A stored series' first occurrence
42
+ starts at the instant stored, which may be the second pass of a repeated hour. One-off events,
43
+ overrides (moved occurrences) and PERIOD RDATEs occupy exactly the times they give. Display and
44
+ export keep each event as written. Feeds, the trusted importer and the store read events this way
45
+ in every zone, including zones whose rules changed in the past and zones a feed defines only by
46
+ its VTIMEZONE.
47
+
48
+ A series' occurrences never overlap each other. Where one ends after the next starts (24 exact
49
+ hours from midnight run to 01:00 the night the clocks go forward, or an occurrence is longer than
50
+ the series' period), it ends where the next starts, and the next ends at the later of the two ends.
51
+ Together they hold the same time, and the store, which keeps each occurrence as a reservation
52
+ under one EXCLUDE constraint, can hold them. Two that start at the same instant are left as they
53
+ are. An all-day series occupies whole local days, from one local midnight to the next (a midnight
54
+ the clocks skip moves forward).
55
+
56
+ | Case | Example | Occupies |
57
+ | --- | --- | --- |
58
+ | The clocks go forward inside an occurrence | London, daily 00:00-06:00, 2027-03-28 | six exact hours, 00:00Z-06:00Z |
59
+ | The clocks go back inside an occurrence | London, daily 00:00-06:00, 2026-10-25 | seven wall-clock hours, 2026-10-24 23:00Z-06:00Z |
60
+ | The clocks go back, southern hemisphere | Santiago, daily 22:00-04:00, night of 2027-04-03 | seven hours, 2027-04-04 01:00Z-08:00Z |
61
+ | The clocks go forward, southern hemisphere | Auckland, daily 00:00-06:00, 2027-09-26 | six hours, 2027-09-25 12:00Z-18:00Z |
62
+ | A day long, the clocks go forward | London, daily 00:00-00:00 from 2027-03-26 | on 2027-03-28, 00:00Z-23:00Z, where the next day starts |
63
+ | A week long, the clocks go forward | New York, weekly 00:00 for 168 hours from 2027-03-07 | the second week 2027-03-14 05:00Z to 03-21 04:00Z |
64
+ | An all-day series | Santiago, daily from 2027-09-03 | on 2027-09-05, 04:00Z (01:00, midnight being skipped) to 09-06 03:00Z |
65
+ | An occurrence starts in the skipped hour | Berlin, daily 02:30 for 75 minutes, 2027-03-28 | 75 exact minutes from 01:30Z (03:30 CEST) |
66
+ | A half-hour change | Lord Howe, daily 02:00-02:45, 2027-10-03 | 45 minutes from 2027-10-02 15:30Z |
67
+ | DTEND falls in the repeated hour | New York, daily 23:00-01:30 from 2026-10-31 | 2026-11-01 03:00Z to the later 01:30, 06:30Z |
68
+ | DTEND falls in the skipped hour | New York, daily 01:00-02:30 from 2027-03-14 | the first 06:00Z-07:30Z, as written; later days 90 minutes, to the written 02:30 |
69
+ | The first instance spans the change | Berlin, weekly 01:00-04:00 from 2027-03-28 | as written, 00:00Z-02:00Z; later weeks three hours |
70
+ | An occurrence starts in the repeated hour | London, daily 01:15-01:45 from 2026-10-20 | on 2026-10-25, its first 01:15 (00:15Z) to the later 01:45 (01:45Z) |
71
+ | A stored first occurrence in the repeated hour | London, stored from 2026-10-25 01:15Z | the second 01:15, as stored |
72
+ | A length the clocks reduce to nothing | DTSTART and DTEND both in one skipped hour, the end at its close | refused |
73
+
74
+ A series whose DTSTART the clocks skip is read from a feed (from the offset before the gap) but is
75
+ not stored: no instant reads back as its wall time. The same holds for an all-day series whose
76
+ first day's midnight the clocks skip (Santiago and Havana go forward at midnight). The trusted
77
+ importer stores an all-day series as a timed one from local midnight to local midnight, which the
78
+ union rule holds to its exact length too: where the clocks go forward on one of its days, or at the
79
+ midnight its first occurrence ends, it may also hold the hour after a midnight that the feed's days
80
+ do not, never less. The trusted importer does not store a timed series whose DTEND the clocks skip, or
81
+ whose DURATION counts days or weeks outside UTC: kept as its first occurrence's instants, the first
82
+ would hold an hour the feed does not on every later day, and the second on each day it spans that
83
+ the clocks go forward.
20
84
 
21
85
  ## Availability
22
86
 
@@ -24,7 +88,7 @@ Availability is the intersection of explicit bookable windows and the complement
24
88
  No rule means no availability. Unsupported or invalid rules contribute no availability. A next-slot
25
89
  answer must fit wholly inside the resulting free set for the requested positive duration.
26
90
 
27
- Slotlock 0.1 supports weekly RRULE cadence with explicit `BYDAY`, interval 1, optional absolute
91
+ Weekly availability supports RRULE cadence with explicit `BYDAY`, interval 1, optional absolute
28
92
  `UNTIL`, a local start minute, and a positive duration. Rules requiring an authored recurrence anchor
29
93
  such as `COUNT` or interval greater than 1 fail closed.
30
94
 
@@ -33,7 +97,9 @@ such as `COUNT` or interval greater than 1 fail closed.
33
97
  - The persistence layer is the final conflict arbiter; an application pre-check is not authority.
34
98
  - A live hold occupies the same range as a confirmed reservation until its database-clock expiry.
35
99
  - Confirm and release operations are atomic and idempotent for their expected retry cases.
36
- - A trailing buffer extends occupied time without changing the customer-visible end instant.
100
+ - A hold lasts at most 7 days from its creation.
101
+ - A trailing buffer extends occupied time without changing the customer-visible end instant. It is
102
+ at most 30 days, and holds and reservations alike respect it.
37
103
  - External commands use `(tenantRef, externalRef)` as stable identity and a strictly increasing
38
104
  positive revision. A replay with the same revision and payload is idempotent; drift is rejected.
39
105
  - Cancellation leaves a revisioned tombstone so delayed delivery cannot resurrect the identity.
@@ -58,6 +124,13 @@ the provider, map a connection to one resource, normalize status/transparency/re
58
124
  durable cursor and coverage window, and reconcile deletions. iCalendar parsing must remain bounded by
59
125
  input size, components, exceptions, occurrences, work, and expansion horizon.
60
126
 
127
+ A feed's overrides occupy what the provider shows: one without DTEND or DURATION lasts its series'
128
+ length, and one whose RECURRENCE-ID names no occurrence, or whose occurrence the series also
129
+ EXDATEs, occupies its own time. The instances of a CalDAV-expanded series are each read, the first
130
+ with or without a RECURRENCE-ID. An event with both DTEND and DURATION is refused (RFC 5545
131
+ §3.6.1). A reader that can name refused series refuses one series at a time and reads the rest of
132
+ the feed; one that cannot refuses the feed, so a refused series is never read as free time.
133
+
61
134
  Content not required for conflict decisions should be discarded before reaching an agent. Outbound
62
135
  iCalendar updates retain stable UID and monotonic sequence; cancellation is explicit.
63
136
 
@@ -71,12 +144,37 @@ the key with drift fails. Cancellation atomically removes derived occupation and
71
144
  tombstone; delayed create delivery cannot resurrect the UID.
72
145
 
73
146
  Events may carry organizer, attendees, roles, participation status, RSVP requests, display/email
74
- relative reminders, one RFC 5545 RRULE, and bounded recurrence exceptions. A materialization window is
75
- mandatory for recurring events, must not exceed 367 days, and may produce at most 2,000 occurrences
76
- under one 4,000-step work budget. Opaque occurrences are reservations under the same PostgreSQL EXCLUDE
77
- constraint as bookings and holds. Transparent occurrences remain queryable but occupy no resource.
78
- An event update replaces its materialized set transactionally, so an overlap rolls the entire mutation
79
- back and preserves the prior event.
147
+ relative reminders, one RFC 5545 RRULE, and bounded recurrence exceptions. A materialization window
148
+ is mandatory for recurring events, must not exceed 367 days, and may produce at most 2,000
149
+ occurrences. Expansion is bounded by a count of work, never a clock, so an input is accepted or
150
+ refused the same way however loaded the machine: each series may pass 4,000 occurrences from its
151
+ start, and an expansion may take 50,000 steps for an event as it is written and 500,000 for a feed
152
+ or a stored series as maintenance rolls it. A step of ical.js's search costs three, a weekday it
153
+ compares one, a day it walks a fifth, a year it scans three and an eighth for each day that scan
154
+ lists, and a weekday of a year it lists half. Each occurrence passed costs one, each conversion of a
155
+ time in an IANA zone half (one and a half in a zone only a feed defines), and each instant Intl is
156
+ asked for to read a zone's offsets five, charged the first time the expansion reads that day of the
157
+ zone whatever was read before it. Each change listed when a feed's own zone is expanded costs four
158
+ tenths. A zone written the way Outlook writes them, from 1601, costs about 6,000.
159
+
160
+ A stored rule is daily or less frequent, follows RFC 5545 §3.3.10's grammar (every part known and
161
+ given once, `COUNT` at least 1, never `COUNT` with `UNTIL`), and is of a shape the expander reads
162
+ as RFC 5545 does: `BYWEEKNO`, a two-digit `BYDAY` ordinal, `BYSETPOS` other than with
163
+ `FREQ=MONTHLY` and `BYDAY` alone, `BYMONTH` with `FREQ=MONTHLY`, a negative `BYMONTHDAY` other
164
+ than with `FREQ=MONTHLY` and no `BYDAY`, `FREQ=YEARLY` with `BYMONTHDAY` but no `BYMONTH` or with a
165
+ day none of its months has, with `COUNT` or `BYDAY` and a day one of its months can lack, with
166
+ `BYYEARDAY` and `BYMONTH` or `BYMONTHDAY`, or with a numbered `BYDAY` and `BYMONTHDAY`, and time
167
+ parts with `FREQ=MONTHLY` or `YEARLY` are refused, with a reason. A yearly day some years lack (29
168
+ February, or a 31st with April) occurs only in the years that have it (RFC 5545 §3.3.10), so its
169
+ rule must be its series' only source of occurrences, with no RDATE or second RRULE. A DATE `UNTIL`
170
+ on a timed series includes its whole day on the series' clock.
171
+
172
+ A recurrence exception names the occurrence it changes by that occurrence's instant, exported as a
173
+ UTC `RECURRENCE-ID`; one that names no occurrence is refused. Opaque occurrences are reservations
174
+ under the same PostgreSQL EXCLUDE constraint as bookings and holds. Transparent occurrences remain
175
+ queryable but occupy no resource. An event update replaces its materialized set transactionally,
176
+ so an overlap rolls the entire mutation back and preserves the prior event. An exported calendar
177
+ holds a VTIMEZONE (RFC 5545 §3.6.5) for the zone its times name, written from the IANA rules.
80
178
 
81
179
  Full event content is accepted only by the explicit trusted parser. The provider-normalization parser's
82
180
  return type contains only identity, recurrence identity, interval, deletion, busy, and revision. A
@@ -89,7 +187,8 @@ contains the entire window. Any missing, stale, or partial source is returned by
89
187
  silently treated as free time.
90
188
 
91
189
  An event has a finite, positive duration. A one-off event may last up to 3,660 days and its
92
- occupation matches the event exactly; a recurring event's occurrences are materialized as above. An
190
+ occupation matches the event exactly; a recurring event's occurrences are materialized as above,
191
+ each occupying what "Daylight-saving time" says. An
93
192
  all-day (`VALUE=DATE`) iCalendar event covers whole local days of the calendar's timezone, from local
94
193
  midnight to local midnight, taken from the caller's calendar timezone, else the feed's
95
194
  `X-WR-TIMEZONE`; with neither the input is refused, never read in the server's zone. An all-day
@@ -129,19 +228,29 @@ event, an exhausted quota) is a result, not a protocol error:
129
228
  - A2A returns the agent's reply message with one `application/json` data part holding the result or
130
229
  `{"error":{"code":"…"}}`.
131
230
 
132
- Protocol errors stay in the protocol. MCP: an unknown tool is JSON-RPC `-32602`, a server fault
231
+ A body must be `application/json`, compared without regard to case (RFC 9110 §8.3.1), or the request
232
+ is HTTP 415; one longer than the server's limit is HTTP 413, refused as soon as it passes the limit,
233
+ whether or not it declares a `Content-Length`.
234
+
235
+ Protocol errors stay in the protocol. MCP: an unknown tool, or `arguments` that are not an object
236
+ (left out or null is none), is JSON-RPC `-32602`, a server fault
133
237
  (including backend output that violates the declared schema) is `-32603` with HTTP 500, and a
134
238
  missing or unsupported `MCP-Protocol-Version` header is HTTP 400. A 2026-07-28 request is checked in
135
239
  that revision's order, each failure HTTP 400: a missing or malformed `_meta` field is `-32602`, an
136
240
  `MCP-Protocol-Version`, `Mcp-Method` or (Base64-sentinel-decoded) `Mcp-Name` that disagrees with the
137
241
  body is `-32020`, and an unimplemented revision is `-32022` naming the supported ones. An
138
242
  unimplemented method is HTTP 404 `-32601`; a rate-limited request is HTTP 429 `-31029`, outside the
139
- `-32020`..`-32099` range that revision reserves. A2A: every JSON-RPC error travels
140
- in an HTTP 200 envelope with a `google.rpc.ErrorInfo` detail; an `A2A-Version` other than `1.0`,
141
- including none (read as 0.3), is `VersionNotSupportedError` (`-32009`); push-notification methods are
243
+ `-32020`..`-32099` range that revision reserves. A2A: every JSON-RPC error travels in an HTTP 200
244
+ envelope (a body over the size limit is HTTP 413); A2A's own errors, below, carry a
245
+ `google.rpc.ErrorInfo` detail and JSON-RPC's (`-32700`, `-32600`, `-32601`, `-32603`) none. An
246
+ `A2A-Version` other than `1.0`, including none (read as 0.3), is `VersionNotSupportedError`
247
+ (`-32009`); push-notification methods are
142
248
  `PushNotificationNotSupportedError` (`-32003`); streaming, task subscription and the extended card
143
249
  are `UnsupportedOperationError` (`-32004`); a message that is not exactly one data part
144
- `{"skill": "<id>", "arguments": {…}}` is `-32602`. The server replies with messages and keeps no
250
+ `{"skill": "<id>", "arguments": {…}}` (`arguments` left out or null is none; any other value that is
251
+ not an object is not arguments) is `-32602`, as is a `configuration` or `metadata` that is set but
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. The server replies with messages and keeps no
145
254
  tasks, so the core task operations answer as for an unknown task: `GetTask` and `CancelTask` are
146
255
  `TaskNotFoundError` (`-32001`), `ListTasks` returns an empty page (`pageSize` 1-100, default 50).
147
256
  Their params are checked first, as `SendMessage`'s are: a field the method does not define, a
@@ -195,8 +304,10 @@ that span changed, so the passage of time alone announces nothing. It ends a
195
304
  subscription gracefully, with the listen request's result, at its maximum duration, on shutdown, or
196
305
  when a re-read is refused as unauthenticated, unauthorized or not found; it closes the stream without
197
306
  a result after three consecutive failed re-reads or when the reader leaves more than 256 events
198
- unread. Open subscriptions, counted from the moment they start opening, are capped per principal and
199
- per server. A subscription that honors nothing ends right after its acknowledgment.
307
+ unread. Open subscriptions, counted from the moment they start opening, are capped per principal,
308
+ per tenant and per server. When the server is full, a listen from a tenant holding fewer than the
309
+ tenant holding most, less one, ends that tenant's oldest open stream gracefully, as at shutdown,
310
+ rather than being refused. A subscription that honors nothing ends right after its acknowledgment.
200
311
 
201
312
  ### Discovery and authorization
202
313
 
@@ -210,6 +321,32 @@ so each must be a canonical URL without credentials, query or fragment: `https`,
210
321
  when `allowInsecureLocalhost` is set. Publishing metadata grants nothing: the embedder's
211
322
  `authenticate` verifies every token, including its audience.
212
323
 
324
+ ### Scopes, API keys and the application API
325
+
326
+ Every authenticated principal carries a tenant and scopes from `read`, `write` and `admin`. A read
327
+ operation needs `read`; an event write needs `write`. The application API (resources, weekly hours,
328
+ holds, reservations, coverage, every key's events, the tenant itself and maintenance, as REST under
329
+ `/v1` beside the tools) needs `admin`. It is never an MCP tool or an A2A skill and never waits for a
330
+ confirmation. Through it a tenant may cancel any of its events, whichever key wrote it, and, only
331
+ where the server allows it and with its own reference as confirmation, erase itself; it never
332
+ suspends itself or lifts a suspension, which only the deployment role does.
333
+
334
+ An API key is a random secret, stored only as its SHA-256 digest and reached only through
335
+ `SECURITY DEFINER` functions. It acts in the tenant it was created in, with the scopes it was given
336
+ (`read` and `write` by default), until it expires (at most 3,650 days) or is revoked. A tenant holds
337
+ at most 100 active keys and 1,000 in all. The digest of a rotated or erased key is kept, so that key
338
+ is never valid again. A server's own bearer token is never `admin`.
339
+
340
+ A dashboard signs a person in with GitHub, asking for no scope, to the tenant `github:<user id>`,
341
+ and manages that tenant's keys, resources (at most 100) and weekly hours.
342
+
343
+ ### Maintenance
344
+
345
+ Recurring events are materialized up to the rolling horizon. Maintenance extends each series to it
346
+ and deletes commands and tombstones past the retention window, in bounded batches; a run that
347
+ reaches its batch limit says so, and the next run continues. `slotlock serve` maintains each tenant
348
+ it serves every hour; `slotlock maintain` and the application API run it on demand.
349
+
213
350
  ### Principal-scoped events, quotas and retention
214
351
 
215
352
  `createSlotlockStoreAgentBackend` is the canonical binding to the PostgreSQL kernel. It derives an
@@ -237,4 +374,7 @@ rejects absolute-form request targets, applies finite header/body/handler and co
237
374
  propagates disconnects through `AbortSignal`, and force-closes only after a bounded graceful-shutdown
238
375
  window. When it closes it first asks the target to end long-lived responses (the agent server's
239
376
  `shutdown()`), and closes keep-alive connections whose response finished during shutdown. It exposes
240
- sanitized transport failures and leaves public TLS termination to the deployment.
377
+ sanitized transport failures and leaves public TLS termination to the deployment. A body the target
378
+ stops reading (one past its size limit) does not abort the request: once the response is written,
379
+ the rest is discarded as it arrives, within `requestTimeout`. Closing the adapter while it is still
380
+ starting rejects the pending `listen()` with `listen_aborted`.
package/agent-server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type SlotlockTraceContext } from './mcp-modern.js';
2
+ import { type SlotlockApplicationBackend, type SlotlockApplicationOperation } from './application-api.js';
2
3
  export type { SlotlockTraceContext } from './mcp-modern.js';
3
4
  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';
4
5
  /**
@@ -125,7 +126,10 @@ export type SlotlockAgentWriteOperation = Extract<SlotlockAgentOperation, 'slotl
125
126
  * A listed write first answers `input_required` with a yes/no form describing the change; it runs
126
127
  * only when the client retries with the person's explicit acceptance and the sealed state it was
127
128
  * given. Where no confirmation is possible — the 2025 MCP revisions and A2A — a listed write fails
128
- * with `confirmation_required`, never silently runs.
129
+ * with `confirmation_required`, never silently runs. The acceptance comes from the client, which a
130
+ * key holder controls: confirmation keeps an agent from writing unasked, and is not an
131
+ * authorization boundary (scopes and `authorize` are). Off unless set; `slotlock serve` sets it only
132
+ * when SLOTLOCK_CONFIRM_WRITES opts in.
129
133
  */
130
134
  export interface SlotlockAgentConfirmationOptions {
131
135
  operations: readonly SlotlockAgentWriteOperation[];
@@ -154,6 +158,12 @@ export interface SlotlockAgentSubscriptionOptions {
154
158
  keepAliveMs?: number;
155
159
  /** Open subscriptions one principal may hold: 1-64, default 4. */
156
160
  maxPerPrincipal?: number;
161
+ /**
162
+ * Open subscriptions one tenant may hold across all its principals: 1-10,000, default `maxTotal`.
163
+ * Set it below `maxTotal` on a multi-tenant server, so a tenant with many keys cannot hold every
164
+ * slot.
165
+ */
166
+ maxPerTenant?: number;
157
167
  /** Open subscriptions across the server: 1-10,000, default 256. */
158
168
  maxTotal?: number;
159
169
  }
@@ -163,15 +173,36 @@ export type SlotlockAgentServerEvent = {
163
173
  operation: SlotlockAgentOperation;
164
174
  outcome: 'requested' | 'accepted' | 'declined' | 'cancelled' | 'expired' | 'refused' | 'unavailable';
165
175
  } | {
166
- /** A REST call that reached an operation, and the HTTP status it answered. */
176
+ /**
177
+ * A REST call that named an operation, and the HTTP status it answered: run, or refused before
178
+ * it ran (a body that is not JSON, too large or not an object).
179
+ */
167
180
  type: 'rest';
168
- operation: SlotlockAgentOperation;
181
+ operation: SlotlockAgentOperation | SlotlockApplicationOperation;
169
182
  status: number;
183
+ /** How long the call took from naming its operation, in milliseconds to a tenth. */
184
+ durationMs: number;
185
+ /** The W3C trace id of the call's valid `traceparent`, when it carried one. */
186
+ traceId?: string;
187
+ } | {
188
+ /** An MCP `tools/call` or A2A `SendMessage` that named an operation, and how it ended. */
189
+ type: 'call';
190
+ protocol: 'mcp' | 'a2a';
191
+ operation: SlotlockAgentOperation;
192
+ /**
193
+ * `ok`, or the refusal's code (`overlap`, `forbidden`, `rate_limited`, …), or why the request
194
+ * was refused before the operation ran: `invalid_arguments` (MCP arguments that are not an
195
+ * object), `missing_capability` (a guarded write from a 2026-07-28 client without form
196
+ * elicitation), `invalid_message` or `invalid_message_parameters` (A2A).
197
+ */
198
+ outcome: string;
199
+ durationMs: number;
200
+ traceId?: string;
170
201
  } | {
171
202
  type: 'subscription';
172
203
  outcome: 'opened' | 'closed';
173
204
  /** Why a subscription closed. */
174
- reason?: 'client' | 'duration' | 'shutdown' | 'revoked' | 'failed' | 'empty';
205
+ reason?: 'client' | 'duration' | 'shutdown' | 'revoked' | 'failed' | 'empty' | 'preempted';
175
206
  /** How many calendar resources it watched. */
176
207
  resources: number;
177
208
  };
@@ -204,6 +235,21 @@ export interface SlotlockAgentServerOptions {
204
235
  * to ask a person. Off unless set.
205
236
  */
206
237
  rest?: boolean;
238
+ /**
239
+ * Also serve the application API under `/v1` (application-api.ts): resources, bookable hours,
240
+ * holds, reservations, calendar coverage and maintenance, for the application around the agents,
241
+ * in any language. Needs `rest`. A principal whose `scopes` are set needs `admin`
242
+ * (SLOTLOCK_APPLICATION_SCOPE); then `authorize` decides. They are never MCP tools or A2A skills,
243
+ * and never wait for a confirmation. Off unless set.
244
+ */
245
+ application?: {
246
+ backend: SlotlockApplicationBackend;
247
+ authorize(args: {
248
+ principal: SlotlockAgentPrincipal;
249
+ operation: SlotlockApplicationOperation;
250
+ input: Record<string, unknown>;
251
+ }): Promise<boolean>;
252
+ };
207
253
  /** Require a person's confirmation before the listed writes run. Off unless set. */
208
254
  confirmation?: SlotlockAgentConfirmationOptions;
209
255
  /**
@@ -218,23 +264,24 @@ export interface SlotlockAgentServerOptions {
218
264
  */
219
265
  subscriptions?: SlotlockAgentSubscriptionOptions | false;
220
266
  /**
221
- * Observe confirmations and subscriptions (counts and outcomes, no arguments or identities) for
222
- * metrics or audit. It runs synchronously and whatever it throws is ignored.
267
+ * Observe calls, confirmations and subscriptions (counts and outcomes, no arguments or
268
+ * identities) for metrics or audit. It is not awaited, and whatever it throws, or its promise
269
+ * rejects with, is ignored.
223
270
  */
224
271
  onEvent?: (event: SlotlockAgentServerEvent) => void;
225
272
  /**
226
273
  * Called with a failure no client is told about: an operation's backend threw something other
227
274
  * than a `SlotlockAgentOperationError`, or returned a result outside the operation's schema. The
228
275
  * client gets `internal_error` and nothing more, so log it here to find out why. It receives the
229
- * error and the operation, never the arguments or the request; it runs synchronously and whatever
230
- * it throws is ignored.
276
+ * error and the operation, never the arguments or the request. It is not awaited, and whatever it
277
+ * throws, or its promise rejects with, is ignored.
231
278
  */
232
279
  onError?: (error: unknown, context: SlotlockAgentErrorContext) => void;
233
280
  }
234
281
  /** What `onError` is told about an unexpected failure besides the error itself. */
235
282
  export interface SlotlockAgentErrorContext {
236
283
  /** The operation that failed; `null` when the failure came before one was resolved. */
237
- operation: SlotlockAgentOperation | null;
284
+ operation: SlotlockAgentOperation | SlotlockApplicationOperation | null;
238
285
  }
239
286
  export interface SlotlockAgentOperationDispatchOptions {
240
287
  backend: SlotlockAgentCalendarBackend;
@@ -309,10 +356,29 @@ export declare function slotlockMcpToolResult(outcome: {
309
356
  code: string;
310
357
  details?: Readonly<Record<string, string | number | boolean | null>>;
311
358
  }): SlotlockMcpToolResult;
359
+ /** The application operations with their input and output as JSON Schema, as REST serves them. */
360
+ export declare function slotlockApplicationOperations(): {
361
+ name: SlotlockApplicationOperation;
362
+ title: string;
363
+ description: string;
364
+ method: "DELETE" | "GET" | "POST" | "PUT";
365
+ path: string;
366
+ inputSchema: Readonly<Record<string, unknown>>;
367
+ outputSchema: Readonly<Record<string, unknown>>;
368
+ }[];
369
+ /**
370
+ * The OpenAPI 3.1 document of the REST API: what a server with `rest` (and `application`) serves at
371
+ * /openapi.json. Without `publicBaseUrl` the server URL is a variable, `{baseUrl}`, as in the
372
+ * repository's openapi.json, which client generators read.
373
+ */
374
+ export declare function slotlockOpenApi(options?: {
375
+ publicBaseUrl?: string;
376
+ application?: boolean;
377
+ }): Record<string, unknown>;
312
378
  export declare function slotlockAgentTools(): {
313
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";
314
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";
315
- description: "Apply an expected-revision event patch and rematerialize occupancy atomically." | "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.";
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.";
316
382
  inputSchema: Readonly<Record<string, unknown>>;
317
383
  outputSchema: Readonly<Record<string, unknown>>;
318
384
  annotations: {