dopus 0.2.10 → 0.2.12

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/README.md CHANGED
@@ -142,6 +142,7 @@ export default defineAgent({
142
142
  | `input` | **Required.** A zod schema. The model's arguments are validated before `fn` runs. |
143
143
  | `confirm` | `true`, or `(input, ctx) => boolean`, to ask the user before each call. Use it for anything that writes. |
144
144
  | `output` | `(result) => unknown`. Trims the result before the model sees it, for example to drop internal fields. |
145
+ | `public` | `true` to offer the tool to users who are not signed in too. Needs `identify` (see below). |
145
146
 
146
147
  `ctx` is `{ user, toolCallId, abortSignal }`. `user` is only set when you configure `identify` (see below).
147
148
 
@@ -202,6 +203,33 @@ Your part, so an injected instruction cannot cause damage:
202
203
 
203
204
  We tested this with a real model in a small task-board app. A member wrote a task whose title ordered the assistant to delete other tasks, then an admin's assistant listed the tasks. Claude (claude-sonnet-5-5) did not even request the deletion and pointed out the planted text. If a model did request it, the approval card would still stop it. Other models were not tested this way.
204
205
 
206
+ **Signed-in and anonymous users.** Define `identify` and dopus knows who is signed in. When it returns `undefined`, the
207
+ model gets only the tools marked `public: true` (product info, opening hours, "am I signed in?"), and the prompt says
208
+ the rest needs sign-in, so the agent tells the visitor to sign in instead of trying. Member tools are not offered at
209
+ all, so a visitor does not see which features exist. Signed-in users get every tool. An approval for a member tool
210
+ that arrives after the user signed out (in another tab) is not run.
211
+
212
+ ```ts
213
+ defineAgent({
214
+ tools: {
215
+ getOpeningHours: defineTool(getOpeningHours, {
216
+ description: '...',
217
+ input: z.object({}),
218
+ public: true,
219
+ }),
220
+ listMyOrders: defineTool(() => listMyOrders(), { description: '...', input: z.object({}) }),
221
+ },
222
+ identify: async () => {
223
+ const session = await getSession()
224
+ return session ? { id: session.userId } : undefined
225
+ },
226
+ })
227
+ ```
228
+
229
+ This decides what the model sees, nothing more: keep the checks in your server functions. Without `identify` dopus
230
+ cannot tell, so every tool is offered to everyone (and `defineAgent` warns if you used `public`). Anonymous users cost
231
+ model tokens too; rate-limit them in `beforeRequest` (see [Audit log, usage and rate limits](#audit-log-usage-and-rate-limits)).
232
+
205
233
  **Running tools outside the request.** Background jobs and queues lose the request's cookies. For that case, define `identify(request)` to resolve a serialisable user. Tools then receive it as `ctx.user` and can pass it on explicitly. dopus itself always runs tools inside the request; this is for jobs your tools hand off.
206
234
 
207
235
  ## Prompts
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as PROVIDERS, t as VERSION } from "./version-CWmiLdVn.js";
2
+ import { n as PROVIDERS, t as VERSION } from "./version-gbic1Mgf.js";
3
3
  import { execFileSync, spawn } from "node:child_process";
4
4
  import { parseArgs } from "node:util";
5
5
  import * as p from "@clack/prompts";
package/dist/index.d.ts CHANGED
@@ -66,6 +66,12 @@ interface ToolOptions<SCHEMA extends z.ZodType, RESULT> {
66
66
  * the job finishes the agent is told, reads the result with another of your tools, and continues.
67
67
  */
68
68
  job?: (result: Awaited<RESULT>) => string;
69
+ /**
70
+ * Also offered to users who are not signed in. Needs `identify` in the agent config: when it returns
71
+ * `undefined`, only public tools are given to the model, so features that need an account stay out of sight.
72
+ * This only decides which tools the model sees; keep the permission checks in your server functions.
73
+ */
74
+ public?: boolean;
69
75
  }
70
76
  type ToolFunction<INPUT, RESULT> = (input: INPUT, ctx: ToolContext) => RESULT;
71
77
  interface DopusTool<SCHEMA extends z.ZodType = z.ZodType, RESULT = unknown> {
@@ -235,5 +241,5 @@ interface ProviderInfo {
235
241
  }
236
242
  /** Verified against @ai-sdk/openai@4.0.111, @ai-sdk/anthropic@4.0.99, @ai-sdk/google@4.0.114. */
237
243
  export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
238
- export declare const VERSION = "0.2.10";
244
+ export declare const VERSION = "0.2.12";
239
245
  export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, FinishEvent, HandlerOptions, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolCallEvent, ToolContext, ToolOptions };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as PROVIDERS, t as VERSION } from "./version-CWmiLdVn.js";
1
+ import { n as PROVIDERS, t as VERSION } from "./version-gbic1Mgf.js";
2
2
  import { convertToModelMessages, createUIMessageStreamResponse, getToolName, isStepCount, isToolUIPart, safeValidateUIMessages, streamText, toUIMessageStream, tool } from "ai";
3
3
  // src/job.ts
4
4
  /**
@@ -82,7 +82,8 @@ const CACHE_BREAKPOINT = { anthropic: { cacheControl: { type: "ephemeral" } } };
82
82
  const RUNTIME_LABELS = {
83
83
  locale: "Response language",
84
84
  role: "User role",
85
- page: "Current page (reported by the browser)"
85
+ page: "Current page (reported by the browser)",
86
+ access: "Access"
86
87
  };
87
88
  /** Picks `ko-KR`, then `ko`, from the locale notes. */
88
89
  function pickLocaleNotes(notes, locale) {
@@ -194,8 +195,27 @@ function defineAgent(config) {
194
195
  if (!isDopusTool(value)) throw new TypeError(`defineAgent: tools.${name} must be created with defineTool()`);
195
196
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name)) throw new TypeError(`defineAgent: tool name "${name}" must match [a-zA-Z0-9_-]{1,64}`);
196
197
  }
198
+ if (!config.identify && Object.values(config.tools).some((t) => t.options.public)) console.warn("[dopus] `public` tools need `identify` in defineAgent: without it dopus cannot tell who is signed in, so every tool is offered to everyone.");
197
199
  return config;
198
200
  }
201
+ /**
202
+ * The tools this request may use: all of them for a signed-in user, only `public` ones for an anonymous user.
203
+ * Without `identify` dopus cannot tell who is signed in, so every tool is offered (the server functions decide).
204
+ */
205
+ function visibleTools(config, user) {
206
+ if (!config.identify || user) return {
207
+ tools: config.tools,
208
+ hidden: []
209
+ };
210
+ const tools = {};
211
+ const hidden = [];
212
+ for (const [name, t] of Object.entries(config.tools)) if (t.options.public) tools[name] = t;
213
+ else hidden.push(name);
214
+ return {
215
+ tools,
216
+ hidden
217
+ };
218
+ }
199
219
  /** Runs a user hook; a failing hook is reported but never breaks the chat. */
200
220
  async function runHook(hook, event, report) {
201
221
  if (!hook) return;
@@ -277,6 +297,10 @@ function readClientContext(body, request) {
277
297
  page: clean(raw.page, 200)
278
298
  };
279
299
  }
300
+ /** Reason recorded for a confirmation of a tool that needs sign-in, answered by a user who is not signed in. */
301
+ const SIGN_IN_REASON = "Not run: this needs the user to be signed in, and they are not (anymore). Tell them to sign in first.";
302
+ /** Runtime prompt line for anonymous users when some tools are hidden from them. */
303
+ const SIGNED_OUT_NOTE = "Not signed in. Only features that work without an account are available; if the user asks for something else, tell them to sign in first.";
280
304
  /** Reason recorded for a confirmation the conversation moved past (the model reads it). */
281
305
  const NOT_RUN_REASON = "Not run: the user sent a new message instead of approving it. If they still want it, call the tool again so they can approve it.";
282
306
  /**
@@ -286,11 +310,11 @@ const NOT_RUN_REASON = "Not run: the user sent a new message instead of approvin
286
310
  * approved-but-unrun call makes every later request fail with a missing tool result. A stale approval is never
287
311
  * executed: the user has moved on, so it may no longer be what they want.
288
312
  */
289
- function settleStaleApprovals(messages) {
313
+ function settleStaleApprovals(messages, hidden = []) {
290
314
  const last = messages.length - 1;
291
315
  return messages.map((message, index) => {
292
- if (message.role !== "assistant" || index === last) return message;
293
- const pending = (part) => isToolUIPart(part) && (part.state === "approval-requested" || part.state === "approval-responded");
316
+ if (message.role !== "assistant") return message;
317
+ const pending = (part) => isToolUIPart(part) && (part.state === "approval-requested" || part.state === "approval-responded") && (index !== last || hidden.includes(getToolName(part)));
294
318
  if (!message.parts.some(pending)) return message;
295
319
  return {
296
320
  ...message,
@@ -303,7 +327,7 @@ function settleStaleApprovals(messages) {
303
327
  approval: {
304
328
  id: part.approval.id,
305
329
  approved: false,
306
- reason: declined ? part.approval.reason : NOT_RUN_REASON
330
+ reason: declined ? part.approval.reason : index === last ? SIGN_IN_REASON : NOT_RUN_REASON
307
331
  }
308
332
  };
309
333
  })
@@ -343,14 +367,15 @@ function createAgentHandler(config, options = {}) {
343
367
  return errorResponse(400, "Request body must be JSON");
344
368
  }
345
369
  const user = await config.identify?.(request);
346
- const { tools, toolApproval } = buildTools(config.tools, {
370
+ const visible = visibleTools(config, user);
371
+ const { tools, toolApproval } = buildTools(visible.tools, {
347
372
  user,
348
373
  report,
349
374
  onToolCall: config.onToolCall
350
375
  });
351
376
  const validated = await safeValidateUIMessages({ messages: body?.messages });
352
377
  if (!validated.success) return errorResponse(400, "Invalid messages");
353
- const messages = settleStaleApprovals(validated.data);
378
+ const messages = settleStaleApprovals(validated.data, visible.hidden);
354
379
  if (messages.some((message) => message.role === "user" && message.parts.some((part) => part.type === "text" && part.text.length > maxMessageChars))) return errorResponse(413, "Message is too long");
355
380
  if (messages.some((message) => message.parts.some((part) => part.type === "data-dopus-job" && !isJobId(part.data?.id)))) return errorResponse(400, "Invalid job notice");
356
381
  const early = await config.beforeRequest?.({
@@ -372,6 +397,7 @@ function createAgentHandler(config, options = {}) {
372
397
  const client = readClientContext(body, request);
373
398
  const runtime = {
374
399
  ...client,
400
+ ...visible.hidden.length > 0 ? { access: SIGNED_OUT_NOTE } : {},
375
401
  ...await config.runtime?.(request, {
376
402
  user,
377
403
  client
@@ -27,5 +27,5 @@ const PROVIDERS = {
27
27
  }
28
28
  };
29
29
  // src/version.ts
30
- const VERSION = "0.2.10";
30
+ const VERSION = "0.2.12";
31
31
  export { PROVIDERS as n, VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dopus",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "A lightweight agent for Next.js: your existing server functions become its tools, run with the signed-in user's permissions.",
5
5
  "license": "MIT",
6
6
  "author": "akodm",