slotlock 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +194 -0
- package/README.md +159 -66
- package/SECURITY.md +26 -13
- package/SPEC.md +10 -7
- package/agent-server.d.ts +47 -4
- package/agent-server.js +492 -147
- package/agent-store-backend.d.ts +9 -0
- package/agent-store-backend.js +83 -15
- package/api-keys.d.ts +12 -2
- package/api-keys.js +36 -7
- package/application-api.d.ts +7 -1
- package/application-api.js +70 -7
- package/assistant.js +2 -1
- package/booking-rules.d.ts +61 -9
- package/booking-rules.js +106 -16
- package/change-watch.d.ts +56 -0
- package/change-watch.js +213 -0
- package/dashboard.d.ts +3 -1
- package/dashboard.js +98 -15
- package/ddl.d.ts +21 -4
- package/ddl.js +555 -7
- package/event-feed.d.ts +54 -4
- package/event-feed.js +107 -8
- package/index.d.ts +8 -7
- package/index.js +5 -4
- package/maintenance.js +12 -10
- package/openapi.json +1309 -82
- package/package.json +1 -1
- package/rest-api.js +12 -0
- package/self-host.js +58 -6
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +35 -3
- package/store.js +447 -135
- package/types.d.ts +14 -1
- package/webhooks.d.ts +10 -2
- package/webhooks.js +11 -3
package/SECURITY.md
CHANGED
|
@@ -22,19 +22,12 @@ Do not include real customer, credential, calendar, or booking data. We will ack
|
|
|
22
22
|
within three business days and coordinate disclosure after a fix is available. Fixed vulnerabilities
|
|
23
23
|
are published as advisories at <https://github.com/PILOTAAI/slotlock/security/advisories>.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Hosted Slotlock
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
27
|
+
The hosted service is not open; self-hosting is the only supported path today. Do not test
|
|
28
|
+
`slotlock-api.pylota.io` or `app.slotlock.pylota.io`: they are not in scope until this section
|
|
29
|
+
says the service is open and gives the rules for testing it. Test Slotlock by running it yourself,
|
|
30
|
+
and report what you find as above.
|
|
38
31
|
|
|
39
32
|
## Security model
|
|
40
33
|
|
|
@@ -84,7 +77,10 @@ earlier, `PUBLIC` may still hold `CREATE` on `public`; the check then refuses ev
|
|
|
84
77
|
a lock that blocks other tenants: that is availability, not isolation. `slotlock serve` checks the
|
|
85
78
|
role it connects as before it serves, and refuses to start as a superuser or a `BYPASSRLS` role
|
|
86
79
|
(`unsafe_serving_role`); it does not repeat the rest of this check, so that a single owning role can
|
|
87
|
-
still migrate and serve (`serve --migrate` without `SLOTLOCK_MIGRATE_DATABASE_URL`).
|
|
80
|
+
still migrate and serve (`serve --migrate` without `SLOTLOCK_MIGRATE_DATABASE_URL`). Row-level
|
|
81
|
+
security does not confine the owner on tables that do not force it, and the owner can lift it, so
|
|
82
|
+
`serve` logs a `serving_as_schema_owner` warning at startup when its role owns the `slotlock`
|
|
83
|
+
schema: serve as a role that owns nothing in production.
|
|
88
84
|
|
|
89
85
|
Schema changes run under a separate deployment role, which must own the `slotlock` schema and every
|
|
90
86
|
object in it (`applySchema` creates the schema when it is missing). The deployment writes rows into
|
|
@@ -105,6 +101,23 @@ as the deployment role or a superuser, or trust its owner as much. `SLOTLOCK_COR
|
|
|
105
101
|
caller's `search_path` at their end; apply each in one transaction (`psql --single-transaction`),
|
|
106
102
|
since under autocommit the pin ends with the statement that sets it.
|
|
107
103
|
|
|
104
|
+
The change feed, the history and webhooks name a coverage source by its `source_id`, never the
|
|
105
|
+
source itself, which can be an iCalendar URL with a token or an address. The id is random (`src_` and
|
|
106
|
+
32 hex digits from `gen_random_uuid()`), stored with the source when it is first recorded and kept
|
|
107
|
+
until it is deleted, so someone who sees a change and knows the tenant cannot confirm a guessed
|
|
108
|
+
source against it (0.5.3's id was an unkeyed digest of the tenant and the source, and could be).
|
|
109
|
+
The tenant reads the id beside the source from `listCoverage`. Upgrading renames the coverage
|
|
110
|
+
changes earlier releases wrote, which carried the source, its last error or that digest, and a
|
|
111
|
+
trigger on the outbox puts one an earlier release's server (0.3-0.5) still running writes after the
|
|
112
|
+
upgrade in the same shape as it is inserted, so none is stored with its source. Every read path serves a `coverage.*` change without `source` and `last_error`, named
|
|
113
|
+
`src_redacted` when its subject is not such an id, and history by subject never matches a coverage
|
|
114
|
+
change by any other name. What was already sent stays sent: a webhook receiver keeps an earlier
|
|
115
|
+
delivery, so rotate a token that an earlier release delivered in a `coverage.stale` change. A
|
|
116
|
+
0.5.3 `source_id` already forwarded (a webhook delivery, a feed reader's copy, a log) still reveals
|
|
117
|
+
its source to anyone who guesses the source and knows the tenant, as it did then; the rename changes
|
|
118
|
+
only the copies in the outbox. `slotlockCoverageSourceId`, which worked that digest out, throws
|
|
119
|
+
since 0.6.
|
|
120
|
+
|
|
108
121
|
The privacy-minimised provider parser cannot return summaries, locations, notes, organizers or
|
|
109
122
|
attendees. Full event parsing is a separate trusted API and must only receive authenticated content.
|
|
110
123
|
Event command rows retain payload digests and stable results, never duplicate event content. Event
|
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. */
|
|
@@ -348,6 +374,12 @@ export interface SlotlockAgentOperationDispatchOptions {
|
|
|
348
374
|
/** As `SlotlockAgentServerOptions.onError`: a failure the result reports only as internal. */
|
|
349
375
|
onError?: (error: unknown, context: SlotlockAgentErrorContext) => void;
|
|
350
376
|
}
|
|
377
|
+
/** One reason `invalid_arguments` refused a call: where in the arguments, and what is wrong there. */
|
|
378
|
+
export interface SlotlockArgumentIssue {
|
|
379
|
+
/** The field's path in the arguments, outermost first (`["rules", 0, "rrule"]`); `[]` is the arguments as a whole. */
|
|
380
|
+
path: (string | number)[];
|
|
381
|
+
message: string;
|
|
382
|
+
}
|
|
351
383
|
export type SlotlockAgentOperationDispatchResult = {
|
|
352
384
|
ok: true;
|
|
353
385
|
data: Record<string, unknown>;
|
|
@@ -356,6 +388,8 @@ export type SlotlockAgentOperationDispatchResult = {
|
|
|
356
388
|
status: number;
|
|
357
389
|
code: string;
|
|
358
390
|
details?: Readonly<Record<string, string>>;
|
|
391
|
+
/** With `invalid_arguments` from argument validation: the fields it refused. */
|
|
392
|
+
issues?: readonly SlotlockArgumentIssue[];
|
|
359
393
|
};
|
|
360
394
|
/** Runtime type guard for the current operation names (legacy dotted names are not included). */
|
|
361
395
|
export declare function isSlotlockAgentOperation(name: unknown): name is SlotlockAgentOperation;
|
|
@@ -393,6 +427,7 @@ export declare function slotlockMcpToolResult(outcome: {
|
|
|
393
427
|
ok: false;
|
|
394
428
|
code: string;
|
|
395
429
|
details?: Readonly<Record<string, string | number | boolean | null>>;
|
|
430
|
+
issues?: readonly SlotlockArgumentIssue[];
|
|
396
431
|
}): SlotlockMcpToolResult;
|
|
397
432
|
/** The application operations with their input and output as JSON Schema, as REST serves them. */
|
|
398
433
|
export declare function slotlockApplicationOperations(): {
|
|
@@ -449,6 +484,8 @@ export declare function invokeSlotlockAgentOperation(args: {
|
|
|
449
484
|
options: SlotlockAgentOperationDispatchOptions;
|
|
450
485
|
/** Validated W3C trace context to hand the backend (see `SlotlockAgentInvocationContext.trace`). */
|
|
451
486
|
trace?: SlotlockTraceContext;
|
|
487
|
+
/** The surface the call came through, handed to the backend (`SlotlockAgentInvocationContext.origin`). */
|
|
488
|
+
origin?: SlotlockChangeOrigin;
|
|
452
489
|
}): Promise<SlotlockAgentOperationDispatchResult>;
|
|
453
490
|
/**
|
|
454
491
|
* Read an A2A `google.protobuf.Timestamp` in its JSON form, as `ListTasks.statusTimestampAfter`
|
|
@@ -478,6 +515,12 @@ export declare function describeSlotlockAgentWrite(args: {
|
|
|
478
515
|
input: Record<string, unknown>;
|
|
479
516
|
signal: AbortSignal;
|
|
480
517
|
}): Promise<string>;
|
|
518
|
+
/**
|
|
519
|
+
* The A2A skill that watches calendars: `SendStreamingMessage` only. Its stream opens with a task
|
|
520
|
+
* in TASK_STATE_WORKING and carries a status update for each change to a watched resource's
|
|
521
|
+
* free/busy, from the same feed as MCP resource subscriptions, until the watch's duration is up.
|
|
522
|
+
*/
|
|
523
|
+
export declare const SLOTLOCK_A2A_WATCH_SKILL = "slotlock_watch_resources";
|
|
481
524
|
export declare function createSlotlockAgentServer(options: SlotlockAgentServerOptions): {
|
|
482
525
|
fetch(request: Request): Promise<Response>;
|
|
483
526
|
manifest: Readonly<Record<string, unknown>>;
|