slotlock 0.5.1 → 0.5.2

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/config.d.ts CHANGED
@@ -36,6 +36,16 @@ export interface SlotlockServeDashboardConfig {
36
36
  organizations?: string[];
37
37
  /** What the deployment is called in the dashboard (SLOTLOCK_DASHBOARD_NAME). */
38
38
  name?: string;
39
+ /**
40
+ * Seals each tenant's model API key for the dashboard assistant (SLOTLOCK_ASSISTANT_SECRET).
41
+ * Without it the assistant is off.
42
+ */
43
+ assistantSecret?: string;
44
+ /**
45
+ * Let the assistant reach a model at a local or private network address
46
+ * (SLOTLOCK_ASSISTANT_PRIVATE_URLS=on): a self-hosted model on the same machine or network.
47
+ */
48
+ assistantPrivateUrls?: boolean;
39
49
  }
40
50
  /**
41
51
  * The dashboard's settings, or undefined when none is set. Setting any of them means serving the
package/config.js CHANGED
@@ -134,6 +134,15 @@ export function readDashboardConfig(env) {
134
134
  if (users === undefined || (users !== '*' && !/^[1-9]\d{0,19}(,[1-9]\d{0,19})*$/.test(users))) {
135
135
  throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_USERS is required to serve the dashboard: * for every GitHub account, or a comma-separated list of GitHub user ids (gh api users/<login> --jq .id)');
136
136
  }
137
+ const assistantSecret = readSecret(env, 'SLOTLOCK_ASSISTANT_SECRET');
138
+ if (assistantSecret !== undefined &&
139
+ [sessionSecret, ...others].includes(assistantSecret)) {
140
+ throw new SlotlockConfigError('SLOTLOCK_ASSISTANT_SECRET must differ from every other secret');
141
+ }
142
+ const privateUrls = optional(env, 'SLOTLOCK_ASSISTANT_PRIVATE_URLS') ?? 'off';
143
+ if (privateUrls !== 'on' && privateUrls !== 'off') {
144
+ throw new SlotlockConfigError('SLOTLOCK_ASSISTANT_PRIVATE_URLS must be on or off');
145
+ }
137
146
  const orgs = readDashboardOrganizations(env);
138
147
  const name = optional(env, 'SLOTLOCK_DASHBOARD_NAME')?.trim();
139
148
  if (name !== undefined && name !== '' && !SLOTLOCK_DASHBOARD_NAME_PATTERN.test(name)) {
@@ -146,6 +155,8 @@ export function readDashboardConfig(env) {
146
155
  allowedUsers: users === '*' ? '*' : [...new Set(users.split(','))],
147
156
  ...(orgs !== undefined ? { organizations: orgs } : {}),
148
157
  ...(name !== undefined && name !== '' ? { name } : {}),
158
+ ...(assistantSecret !== undefined ? { assistantSecret } : {}),
159
+ ...(assistantSecret !== undefined && privateUrls === 'on' ? { assistantPrivateUrls: true } : {}),
149
160
  };
150
161
  }
151
162
  /**
package/dashboard.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { SlotlockApiKeyStore } from './api-keys.js';
2
+ import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal } from './agent-server.js';
3
+ import { type SlotlockAssistantStore } from './assistant.js';
4
+ import { type SlotlockRate } from './config.js';
2
5
  import { type SlotlockBookingRules } from './booking-rules.js';
3
6
  import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
4
7
  import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
@@ -41,6 +44,8 @@ export interface SlotlockDashboardResources {
41
44
  list(tenantRef: string, limit: number): Promise<SlotlockWaitlistEntry[]>;
42
45
  leave(tenantRef: string, id: string, actor?: string): Promise<boolean>;
43
46
  };
47
+ /** The tenant's assistant settings and its members' conversations (assistant.ts). */
48
+ assistant?: SlotlockAssistantStore;
44
49
  /** The tenant's webhook endpoints, as the event feed keeps them. */
45
50
  webhooks?: {
46
51
  list(tenantRef: string): Promise<SlotlockWebhookEndpoint[]>;
@@ -74,6 +79,60 @@ export interface SlotlockDashboardState {
74
79
  */
75
80
  sessionEnded(sid: string, expiresAt: number): Promise<boolean>;
76
81
  }
82
+ /**
83
+ * The chat assistant (assistant.ts): each tenant's admins connect a model of their own in Settings,
84
+ * and every member may chat with it. Its reads and approved writes run through the agent operation
85
+ * boundary with `backend`, as the principal `dashboard-assistant:<GitHub login>` of the member's
86
+ * tenant, with `authorize` and `consumeRateLimit` as an agent's are. Needs the resources adapter's
87
+ * `assistant` too.
88
+ */
89
+ export interface SlotlockDashboardAssistantOptions {
90
+ /** Seals each tenant's API key (SLOTLOCK_ASSISTANT_SECRET): 32+ random characters. */
91
+ secret: string;
92
+ backend: SlotlockAgentCalendarBackend;
93
+ /**
94
+ * As the agent server's: false refuses an operation. Every offered operation by default. It also
95
+ * checks a write the model proposes, so it must hold nothing: limits that hold a slot go in
96
+ * `admit`.
97
+ */
98
+ authorize?: SlotlockAgentOperationDispatchOptions['authorize'];
99
+ /**
100
+ * Taken when a read runs and when an approved write runs, never for a proposal: the agent
101
+ * request limits' `admit` (serve's series-write slots), which may hold a slot until the request
102
+ * is answered, or throw a SlotlockAgentOperationError to refuse.
103
+ */
104
+ admit?: (args: {
105
+ principal: SlotlockAgentPrincipal;
106
+ operation: SlotlockAgentOperation;
107
+ input: Record<string, unknown>;
108
+ }) => Promise<void>;
109
+ /** As the agent server's; charged for each read and each approved write, never for a proposal. */
110
+ consumeRateLimit?: SlotlockAgentOperationDispatchOptions['consumeRateLimit'];
111
+ /**
112
+ * Allow a model at a local or private network address (SLOTLOCK_ASSISTANT_PRIVATE_URLS): a
113
+ * base URL whose host is, or resolves to, a loopback, private, link-local, shared, unique-local
114
+ * or metadata address. Off by default: such URLs are refused when saved and at each call.
115
+ */
116
+ privateUrls?: boolean;
117
+ /**
118
+ * Reaches the model providers. By default Node.js's http client, which checks every address a
119
+ * host resolves to as the connection is made (createSlotlockGuardedFetch), so a name rebound to
120
+ * a private address is refused too. A replacement must make that check itself, or reach no
121
+ * private network (a Cloudflare Worker's fetch).
122
+ */
123
+ fetch?: typeof fetch;
124
+ /** The addresses a host resolves to, to refuse a private one when saved: dns.lookup by default. */
125
+ resolve?: (hostname: string) => Promise<string[]>;
126
+ /** Messages and connection tests one member may send: 10 a minute, 10 at once, by default. */
127
+ rate?: SlotlockRate;
128
+ /** One model call's limit (30 s by default) and one message's (90 s). */
129
+ timeoutMs?: number;
130
+ turnTimeoutMs?: number;
131
+ /** Model calls one message may make (8 by default, at most 20). */
132
+ maxSteps?: number;
133
+ /** How long a conversation is kept after its last message: 2 hours by default. */
134
+ conversationTtlMs?: number;
135
+ }
77
136
  export interface SlotlockDashboardOptions {
78
137
  /** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
79
138
  publicUrl: string;
@@ -118,6 +177,8 @@ export interface SlotlockDashboardOptions {
118
177
  state: SlotlockDashboardState;
119
178
  /** The server's bookable hours: shown, not edited, and used by resources without their own. */
120
179
  availability?: readonly WeeklyAvailabilityRule[];
180
+ /** The chat assistant; off unless set. */
181
+ assistant?: SlotlockDashboardAssistantOptions;
121
182
  fetch?: typeof fetch;
122
183
  now?: () => number;
123
184
  /**