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/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
- ## Testing hosted Slotlock
25
+ ## Hosted Slotlock
26
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.
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`); streaming, task subscription and the extended card
249
- are `UnsupportedOperationError` (`-32004`); a message that is not exactly one data part
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. The server replies with messages and keeps no
254
- tasks, so the core task operations answer as for an unknown task: `GetTask` and `CancelTask` are
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 is intentionally synchronous and advertises neither
368
- streaming nor push notifications; an embedder must not claim task lifecycle support unless it
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. The server finds changes by re-reading each
186
- * open subscription's resources through the backend every `pollIntervalMs`, re-running `authenticate`
187
- * and `authorize` each time, so a revoked credential or permission ends the stream.
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
- /** How often an open subscription re-reads its resources, in ms: 1,000-300,000, default 10,000. */
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>>;