@qoniai/qoni 0.4.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 +232 -0
- package/LICENSE +21 -0
- package/README.md +771 -0
- package/README.zh-CN.md +708 -0
- package/dist/index.cjs +2916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1362 -0
- package/dist/index.d.ts +1362 -0
- package/dist/index.js +2866 -0
- package/dist/index.js.map +1 -0
- package/docs/open-source-readiness.md +50 -0
- package/package.json +81 -0
- package/skills/qoni-sdk/SKILL.md +324 -0
- package/skills/qoni-sdk/agents/openai.yaml +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# @qoniai/qoni
|
|
2
|
+
|
|
3
|
+
[](https://nodejs.org/)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://pnpm.io/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Unified Node.js SDK for Qoni Agent delegation, GenAuth user management, GUMem memory, WebAgent automation, web search, and monitoring.
|
|
9
|
+
|
|
10
|
+
`@qoniai/qoni` gives a trusted server one compact way to use Qoni AK/SK. For runtime product calls, your service verifies the GenAuth user, requests short-lived delegation with `delegateToken`, and then uses the returned token in GUMem, WebAgent, Do Anything, Web Search, and Track calls. For GenAuth user management, the SDK exchanges AK/SK for a standard GenAuth management token internally and calls GenAuth v3 users APIs with that token.
|
|
11
|
+
|
|
12
|
+
AK/SK credentials must stay on a trusted server. Do not ship them to browsers, mobile apps, public CLI config, or untrusted Agent runtimes.
|
|
13
|
+
|
|
14
|
+
English | [中文](./README.zh-CN.md)
|
|
15
|
+
|
|
16
|
+
## Why Qoni
|
|
17
|
+
|
|
18
|
+
Agents that perform real work need more than a backend API key. They need a user boundary, explicit scopes, expiry, observable execution, and audit metadata that explains what happened later.
|
|
19
|
+
|
|
20
|
+
Qoni keeps that model small:
|
|
21
|
+
|
|
22
|
+
- One SDK entry: `new QoniAgentKit({ accessKey, secretKey })`.
|
|
23
|
+
- One discovery path: `host` can override the Qoni Console/SDK gateway for private or local deployments, and the SDK reads downstream runtime URLs from `/api/v3/eak/runtime-config`.
|
|
24
|
+
- One delegation entry: call `delegateToken`; silent mode returns `data.token`, while interactive mode returns an authorization URL and later completes on your server.
|
|
25
|
+
- One management path: call `genauth.users.*`, and the SDK exchanges AK/SK for a GenAuth management token before calling GenAuth v3 users APIs.
|
|
26
|
+
- Capability-first namespaces: `genauth`, `gumem`, `webSearch`, `doAnything`, and `track`.
|
|
27
|
+
- Readable scope strings for least-privilege authorization.
|
|
28
|
+
- Typed errors with `requestId`, `traceId`, `auditId`, and `retryable`.
|
|
29
|
+
|
|
30
|
+
The public package and API use the Qoni brand. The deployed server protocol has
|
|
31
|
+
not migrated yet, so internal signed routes remain under `/api/v3/eak/*`, the
|
|
32
|
+
delegation token claim remains `eak_delegation_token`, and existing `eak.*`
|
|
33
|
+
backend error codes are surfaced unchanged. Existing wire claims such as
|
|
34
|
+
`eak_tenant_id` also remain unchanged. Applications should use the public
|
|
35
|
+
Qoni API and must not construct these internal wire values themselves.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @qoniai/qoni
|
|
41
|
+
# or
|
|
42
|
+
pnpm add @qoniai/qoni
|
|
43
|
+
# or
|
|
44
|
+
yarn add @qoniai/qoni
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requirements:
|
|
48
|
+
|
|
49
|
+
- Node.js 18 or later.
|
|
50
|
+
- A server-side runtime with `fetch`.
|
|
51
|
+
- Qoni `accessKey` and `secretKey` created in Qoni Console.
|
|
52
|
+
- For silent runtime product calls, a real GenAuth user id from the userpool bound to the Qoni credential. For smoke tests, pass it as `QONI_USER_ID` or call `qoni.resolveAnyBoundUser()` to grab the first bound user; in application code, resolve it with `currentUser` or your existing server-side user session. Interactive delegation can start without `user` or `userId` because the Qoni Console/BFF resolves the GenAuth login during authorization.
|
|
53
|
+
- For `genauth.users.*` management calls, `QONI_USER_ID` is not required. The SDK uses AK/SK to request a GenAuth management token from Qoni.
|
|
54
|
+
- Optional `host` for private or local Qoni deployments. Leave it unset for hosted Qoni.
|
|
55
|
+
|
|
56
|
+
You do not pass `tenantId` during normal SDK initialization. The AK/SK is already bound to the tenant and application boundary in Qoni.
|
|
57
|
+
|
|
58
|
+
The package ships both ESM and CommonJS builds with TypeScript types, so either
|
|
59
|
+
import style works:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { QoniAgentKit } from "@qoniai/qoni"; // ESM / TypeScript
|
|
63
|
+
const { QoniAgentKit } = require("@qoniai/qoni"); // CommonJS
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## AI Skill
|
|
67
|
+
|
|
68
|
+
Qoni can also be exposed to AI coding tools through a Skill package, so Codex, Claude Code, or internal Agents can follow the same authorization model.
|
|
69
|
+
|
|
70
|
+
Install the Qoni Skill:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx skills add https://github.com/QONIAI/qoni-sdk-node --skill qoni-sdk
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If you only want to install it for Claude Code, specify the Agent:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx skills add https://github.com/QONIAI/qoni-sdk-node --agent claude-code --skill qoni-sdk
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The npm package includes the `skills/` directory, but the recommended Skill install path is still the GitHub repository command above so agents receive the repository-local skill metadata.
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
Start with a server-side SDK instance. AK/SK stays on your trusted server for both management-plane calls and runtime product delegation.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import fs from "node:fs";
|
|
90
|
+
import { QoniAgentKit } from "@qoniai/qoni";
|
|
91
|
+
|
|
92
|
+
const qoni = new QoniAgentKit({
|
|
93
|
+
accessKey: process.env.QONI_ACCESS_KEY!,
|
|
94
|
+
secretKey: process.env.QONI_SECRET_KEY!,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### GenAuth User Management
|
|
99
|
+
|
|
100
|
+
`genauth.users.*` is a management-plane capability. It does not need `QONI_USER_ID` or `delegateToken`; the SDK signs `POST /api/v3/eak/genauth/admin-token` with `{}` internally, receives a GenAuth management token plus `userPoolId`, and then calls GenAuth v3 users APIs.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const users = await qoni.genauth.users.list({
|
|
104
|
+
page: 1,
|
|
105
|
+
limit: 20,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log("GenAuth users:", users.data);
|
|
109
|
+
|
|
110
|
+
const created = await qoni.genauth.users.create<{ userId: string }>({
|
|
111
|
+
username: `sdk-demo-${Date.now()}`,
|
|
112
|
+
password: process.env.GENAUTH_DEMO_USER_PASSWORD!,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const profile = await qoni.genauth.users.get<{ userId: string }>({
|
|
116
|
+
userId: created.data.userId,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await qoni.genauth.users.update({
|
|
120
|
+
userId: profile.data.userId,
|
|
121
|
+
nickname: "SDK demo user",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Optional smoke-test cleanup:
|
|
125
|
+
// await qoni.genauth.users.deleteBatch({ userIds: [profile.data.userId] });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Runtime Product Delegation
|
|
129
|
+
|
|
130
|
+
GUMem, WebAgent, Do Anything, Web Search, and Track act for an end user. Silent delegation calls need a real GenAuth user id from the userpool bound to the Qoni credential, then a `delegateToken` result.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const userId = process.env.QONI_USER_ID!;
|
|
134
|
+
if (!userId) {
|
|
135
|
+
throw new Error("QONI_USER_ID must be a real GenAuth user id from the credential-bound userpool");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// `products` is per-product authorization sugar; `agent` defaults to "sdk".
|
|
139
|
+
const { token } = (
|
|
140
|
+
await qoni.delegateToken({
|
|
141
|
+
user: { id: userId },
|
|
142
|
+
products: ["doAnything"],
|
|
143
|
+
})
|
|
144
|
+
).data;
|
|
145
|
+
|
|
146
|
+
const run = await qoni.doAnything.run({
|
|
147
|
+
token, // passed once — the handle holds it; handle methods never take a token
|
|
148
|
+
prompt: "Open https://en.wikipedia.org/wiki/Singapore and summarize the country's key facts.",
|
|
149
|
+
capture: { screenshots: true },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await run.wait({
|
|
153
|
+
onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
|
|
154
|
+
onInteraction: (i) => { // login / clarify / confirm / take-control
|
|
155
|
+
if (i.can("confirm_signed_in")) return i.confirmSignedIn();
|
|
156
|
+
if (i.can("confirm")) return i.confirm();
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
console.log(result.output);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`run()` returns a `RunHandle` typed to the product: `qoni.deepResearch.run()` is a `RunHandle<DeepResearchEvent>`, so its events are narrowed to what Deep Research actually emits. `capture: { screenshots: true }` opts into per-step screenshots, decoded to bytes for you. `wait()` drives the run to its terminal state and settles the result (`result.raw` carries cost, step counts, token usage).
|
|
163
|
+
|
|
164
|
+
> **Best practice — match `event.type` against the `QoniEventTypes` constants.**
|
|
165
|
+
> Each product handle is typed to its own event set, so in TypeScript a case
|
|
166
|
+
> for another product's event is a compile error and autocomplete shows only
|
|
167
|
+
> the right ones. All the internal churn (supervisor planning, individual
|
|
168
|
+
> clicks/keystrokes, telemetry) folds into `QoniEventTypes.Progress`; once you
|
|
169
|
+
> match a `case`, `event.data` has a fixed shape — a bare value for simple
|
|
170
|
+
> events (`progress` → a string line, `resultsReady` → a number) and a small
|
|
171
|
+
> object for rich ones (`done` → `{ output, succeeded, terminalReason }`). The
|
|
172
|
+
> raw wire types are not exported.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { QoniEventTypes } from "@qoniai/qoni";
|
|
176
|
+
|
|
177
|
+
const run = await qoni.deepResearch.run({ token, prompt: "State of EU battery recycling, 2026 update" }); // RunHandle<DeepResearchEvent>
|
|
178
|
+
|
|
179
|
+
for await (const event of run.events()) {
|
|
180
|
+
switch (event.type) {
|
|
181
|
+
case QoniEventTypes.Progress: console.log(event.data); break; // data IS the line (string)
|
|
182
|
+
case QoniEventTypes.Phase: console.log(event.data); break; // data IS the phase name (string)
|
|
183
|
+
case QoniEventTypes.SectionReady: console.log(event.data); break; // data IS the section title (string)
|
|
184
|
+
case QoniEventTypes.Message: console.log(event.data.text); break; // rich → { text, role }
|
|
185
|
+
case QoniEventTypes.Interaction: { // rich → the typed Interaction object
|
|
186
|
+
const i = run.interactionHandle(event.data); // e.g. an outline-approval confirmation
|
|
187
|
+
if (i.can("confirm")) await i.confirm();
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case QoniEventTypes.Done: console.log(event.data.output); break; // rich → terminal
|
|
191
|
+
// case QoniEventTypes.ResultsReady — Web Search only → compile error here
|
|
192
|
+
}
|
|
193
|
+
// event.runId / event.at / event.isTerminal / event.raw (escape hatch)
|
|
194
|
+
}
|
|
195
|
+
// The iterator ends automatically at the terminal (QoniEventTypes.Done) event.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
> **JS note:** the per-product compile-time narrowing is a TypeScript feature.
|
|
199
|
+
> In plain JavaScript there's no type checking, so a wrong-product `case` just
|
|
200
|
+
> never fires (it's not an error). Use TypeScript for the compile-time guard.
|
|
201
|
+
|
|
202
|
+
Power users can match the exact internal wire type via `event.raw.event` (a
|
|
203
|
+
raw string like `"run.action.started"`) — e.g. to tell a specific browser
|
|
204
|
+
action apart inside `progress`. That's the escape hatch, not the everyday API.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
await run.cancel("user stopped the task"); // idempotent — safe on a finished run
|
|
208
|
+
const followUp = await qoni.doAnything.run({
|
|
209
|
+
token,
|
|
210
|
+
prompt: "Now open the Wikipedia page for Malaysia and compare its population with Singapore's.",
|
|
211
|
+
session: run.sessionRef, // reuse the same browser session
|
|
212
|
+
});
|
|
213
|
+
const reattached = await qoni.doAnything.attach(run.id, { token }); // re-attach by run id alone
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Authorization Model
|
|
217
|
+
|
|
218
|
+
### Runtime Discovery
|
|
219
|
+
|
|
220
|
+
`host` points to the Qoni Console/SDK gateway, not directly to GenAuth, GUMem, or WebAgent. Hosted integrations normally leave `host` unset; private and local deployments can pass their own gateway.
|
|
221
|
+
|
|
222
|
+
The SDK signs a request to:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
GET /api/v3/eak/runtime-config
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
This path is an intentional compatibility boundary with the current server; it
|
|
229
|
+
is not the public SDK brand or JavaScript namespace.
|
|
230
|
+
|
|
231
|
+
The response provides runtime base URLs for Qoni, GenAuth, GUMem, and WebAgent. Application code should not configure those product URLs separately for normal usage.
|
|
232
|
+
|
|
233
|
+
### Server-Side AK/SK
|
|
234
|
+
|
|
235
|
+
`accessKey` and `secretKey` identify the tenant and application boundary configured in Qoni Console. The SDK uses them to sign trusted server-side Qoni requests, including delegation and product-token exchange.
|
|
236
|
+
|
|
237
|
+
### Delegation Token
|
|
238
|
+
|
|
239
|
+
In silent mode, `delegateToken` binds four things:
|
|
240
|
+
|
|
241
|
+
- the current user id
|
|
242
|
+
- the Agent id
|
|
243
|
+
- the requested scopes
|
|
244
|
+
- the token expiry and audit context
|
|
245
|
+
|
|
246
|
+
Silent product calls receive `token: delegation.data.token`. If that token is an Qoni delegation token, the SDK exchanges it internally for the correct GUMem or WebAgent product access token before calling the downstream service.
|
|
247
|
+
|
|
248
|
+
**Input shape.** `delegateToken` is discriminated by `mode`:
|
|
249
|
+
|
|
250
|
+
- **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
|
|
251
|
+
is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
|
|
252
|
+
deprecated top-level `userId`. Omitting it throws `QoniValidationError`
|
|
253
|
+
locally before any request. Authorize with the `products` sugar and/or
|
|
254
|
+
explicit `scopes`.
|
|
255
|
+
- **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
|
|
256
|
+
`agent`, `scopes` and **no** user (Qoni Console resolves the login during
|
|
257
|
+
authorization). Returns `data.authorizationUrl`, not a token; complete it
|
|
258
|
+
server-side with `completeDelegateToken({ grantId, code, state })`.
|
|
259
|
+
|
|
260
|
+
`agent` is optional and defaults to `"sdk"`.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const delegation = await qoni.delegateToken({
|
|
264
|
+
userId,
|
|
265
|
+
agent: "research-assistant",
|
|
266
|
+
scopes: ["gumem.memory:read"],
|
|
267
|
+
mode: "silent",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await qoni.gumem.recall({
|
|
271
|
+
token: delegation.data.token,
|
|
272
|
+
query: "research preferences",
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
In interactive mode, your server still starts with `delegateToken`, but it does not receive a token:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const grant = await qoni.delegateToken({
|
|
280
|
+
mode: "interactive",
|
|
281
|
+
redirectUri: "https://app.example.com/qoni/callback",
|
|
282
|
+
state: "business-csrf-state",
|
|
283
|
+
agent: "research-assistant",
|
|
284
|
+
scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
redirectUserTo(grant.data.authorizationUrl);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The user visits `authorizationUrl` in Qoni Console/BFF. If they are not logged in, Console/BFF sends them through GenAuth login, confirms authorization, completes the grant internally, then redirects back to your `redirectUri` with a one-time `code`, your `state`, and `grant_state`/`grantId`. The browser never receives a delegation token.
|
|
291
|
+
|
|
292
|
+
Your callback handler runs on the server and exchanges the callback fields with AK/SK:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const completed = await qoni.completeDelegateToken({
|
|
296
|
+
grantId: String(req.query.grantId ?? req.query.grant_id),
|
|
297
|
+
code: String(req.query.code),
|
|
298
|
+
state: String(req.query.state),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await qoni.gumem.recall({
|
|
302
|
+
token: completed.data.token,
|
|
303
|
+
query: "research preferences",
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`completeDelegateToken` is the recommended server callback helper. `delegateAgent` and `completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
|
|
308
|
+
|
|
309
|
+
## Choosing Scopes
|
|
310
|
+
|
|
311
|
+
Each WebAgent product exposes exactly two scopes: `read` (observe — status, events, artifacts) and `manage` (anything that changes execution state — run, cancel, respond, messages, monitor CRUD). The full set is the 8 scopes `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. The simplest authorization is the `products` sugar — each product name expands to its `read` + `manage` pair:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
await qoni.delegateToken({
|
|
315
|
+
user: { id: userId },
|
|
316
|
+
products: ["doAnything", "webSearch", "deepResearch", "track"],
|
|
317
|
+
scopes: ["gumem.memory:read"], // fine-grained scopes can be mixed in
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Scope strings are validated locally before any request: a scope missing its service prefix (e.g. `do_anything:manage` instead of `webagent.do_anything:manage`) throws `QoniValidationError` immediately with the correct form in the message.
|
|
322
|
+
|
|
323
|
+
Use explicit scope strings when the requested permission boundary should be visible in code.
|
|
324
|
+
|
|
325
|
+
| Scenario | Useful scopes | User-facing meaning |
|
|
326
|
+
| --- | --- | --- |
|
|
327
|
+
| Read user memory | `gumem.memory:read` | Agent can read relevant historical preferences. |
|
|
328
|
+
| Create a GUMem session and recall context | `gumem.memory:read`, `gumem.memory:write`, `gumem.message:write` | Agent can create a memory session, write messages, and recall context for the current user. |
|
|
329
|
+
| Write task results | `gumem.message:write`, `gumem.action:write` | Agent can write this task's confirmed result back to GUMem. |
|
|
330
|
+
| Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
|
|
331
|
+
| Run a bounded web task | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent can run a visible web task, read progress, stop it, and intervene when needed. |
|
|
332
|
+
| Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
|
|
333
|
+
|
|
334
|
+
Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
|
|
335
|
+
|
|
336
|
+
Interactive mode is recommended for higher-risk actions such as browser takeover, external site login, long-running monitoring, form submission, or access to sensitive artifacts and recordings.
|
|
337
|
+
|
|
338
|
+
The SDK also exports a GUMem-oriented bundle for the common session/write/recall path:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
QoniScopeBundles.GUMEM_SESSION_RECALL
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Capability Examples
|
|
345
|
+
|
|
346
|
+
### GUMem
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
await qoni.gumem.createSession({
|
|
350
|
+
token,
|
|
351
|
+
userId,
|
|
352
|
+
sessionId: "daily-assistant",
|
|
353
|
+
title: "Daily assistant memory",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Your application has already collected and confirmed this preference with the user.
|
|
357
|
+
const confirmedPreference =
|
|
358
|
+
"Please keep daily planning suggestions concise and include only the next action.";
|
|
359
|
+
|
|
360
|
+
await qoni.gumem.addMessages({
|
|
361
|
+
token,
|
|
362
|
+
sessionId: "daily-assistant",
|
|
363
|
+
messages: [
|
|
364
|
+
{
|
|
365
|
+
role: "user",
|
|
366
|
+
content: confirmedPreference,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
role: "assistant",
|
|
370
|
+
content: `Saved confirmed user preference: ${confirmedPreference}`,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const context = await qoni.gumem.recall({
|
|
376
|
+
token,
|
|
377
|
+
sessionId: "daily-assistant",
|
|
378
|
+
query: "What confirmed preferences should I consider for this user's next task?",
|
|
379
|
+
details: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Pass context.data into your own assistant response composer or task planner.
|
|
383
|
+
// The SDK example stops here because application orchestration is product-specific.
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Do Anything
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
const run = await qoni.doAnything.run({
|
|
390
|
+
token,
|
|
391
|
+
prompt: "Open https://en.wikipedia.org/wiki/Solid-state_battery and summarize its key advantages over lithium-ion.",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const status = await run.status(); // refresh the run's current state
|
|
395
|
+
await run.cancel("User stopped the task"); // idempotent
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Web Search
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const search = await qoni.webSearch.run({
|
|
402
|
+
token,
|
|
403
|
+
prompt: "EU battery recycling regulations 2026",
|
|
404
|
+
maxResultsPerQuery: 5,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const result = await search.wait();
|
|
408
|
+
console.log(result.output); // search results
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Deep Research
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const research = await qoni.deepResearch.run({
|
|
415
|
+
token,
|
|
416
|
+
prompt: "State of EU battery recycling, 2026 update",
|
|
417
|
+
depth: "standard", // light | standard | deep — the main research knob
|
|
418
|
+
limits: { maxDurationMinutes: 120 }, // wall-clock cap; the run self-terminates past it
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const report = await research.wait();
|
|
422
|
+
for (const artifact of report.artifacts) {
|
|
423
|
+
fs.writeFileSync(artifact.name ?? artifact.id, await artifact.content());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Follow-up research that inherits the prior run's context:
|
|
427
|
+
const followUp = await qoni.deepResearch.run({
|
|
428
|
+
token,
|
|
429
|
+
prompt: "Now compare against the 2025 numbers.",
|
|
430
|
+
session: research.sessionRef,
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Track
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
const monitor = await qoni.track.create({
|
|
438
|
+
token,
|
|
439
|
+
prompt: "Monitor the price of Bitcoin every minute.",
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// A new monitor starts in `pending_clarification` while the backend aligns
|
|
443
|
+
// the intent into a concrete spec, then flips to `active` (usually a few
|
|
444
|
+
// seconds; it may emit an `interaction` first if the intent is ambiguous).
|
|
445
|
+
// runNow()/refine() require `active`, so wait for it before acting:
|
|
446
|
+
for await (const event of monitor.events()) {
|
|
447
|
+
if (event.type === "interaction") {
|
|
448
|
+
const i = monitor.interactionHandle(event.data); // e.g. a needs-reauth site_login
|
|
449
|
+
if (i.can("confirm")) await i.confirm();
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const { status } = await monitor.get();
|
|
453
|
+
if (status === "active") break;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await monitor.runNow(); // trigger an immediate tick
|
|
457
|
+
const ticks = await monitor.runs({ limit: 10 }); // read-only tick run views
|
|
458
|
+
|
|
459
|
+
// Change the definition with refine() (any subset of: schedule / targetUrls /
|
|
460
|
+
// extractionSchema / triggerDsl / stopConditionDsl / notifyChannel). `schedule`
|
|
461
|
+
// is { kind: "cron", expr } or { kind: "interval", intervalSeconds }. The
|
|
462
|
+
// natural-language intent isn't refinable — create a new monitor to change it:
|
|
463
|
+
await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
|
|
464
|
+
await monitor.pause(); // stop scheduled ticks without deleting
|
|
465
|
+
await monitor.resume();
|
|
466
|
+
await monitor.delete();
|
|
467
|
+
|
|
468
|
+
// Reconnect later from a stored id:
|
|
469
|
+
const sameMonitor = await qoni.track.attach(monitor.id, { token });
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### GenAuth User Context and Management
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
const client = new QoniAgentKit({
|
|
476
|
+
accessKey: process.env.QONI_ACCESS_KEY!,
|
|
477
|
+
secretKey: process.env.QONI_SECRET_KEY!,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const profile = await client.genauth.userInfo({
|
|
481
|
+
accessToken: process.env.GENAUTH_ACCESS_TOKEN!,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const users = await client.genauth.users.list({
|
|
485
|
+
page: 1,
|
|
486
|
+
limit: 20,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const created = await client.genauth.users.create({
|
|
490
|
+
username: "sdk-demo-user",
|
|
491
|
+
password: process.env.GENAUTH_DEMO_USER_PASSWORD!,
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
`currentUser` and `genauth.userInfo` use a GenAuth access token because they read the logged-in user's identity. `genauth.users.*` is a management-plane capability, but application code still authenticates the SDK with Qoni AK/SK: the SDK signs `POST /api/v3/eak/genauth/admin-token` with an empty JSON body `{}`, receives a standard GenAuth management token plus `userPoolId`, then calls GenAuth v3 user management APIs with `Authorization: Bearer ...` and `x-authing-userpool-id`. It does not send `resource`, `actions`, `expiresIn`, or `QONI_USER_ID`. Runtime capability calls such as GUMem/WebAgent use `delegateToken` output instead.
|
|
496
|
+
|
|
497
|
+
## API Surface
|
|
498
|
+
|
|
499
|
+
### Client
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const qoni = new QoniAgentKit({
|
|
503
|
+
accessKey: process.env.QONI_ACCESS_KEY!,
|
|
504
|
+
secretKey: process.env.QONI_SECRET_KEY!,
|
|
505
|
+
timeoutMs: 30_000,
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
| Option | Type | Required | Description |
|
|
510
|
+
| --- | --- | --- | --- |
|
|
511
|
+
| `accessKey` | `string` | Yes | Qoni access key from Qoni Console. |
|
|
512
|
+
| `secretKey` | `string` | Yes | Qoni secret key from Qoni Console. |
|
|
513
|
+
| `host` | `string` | No | Optional Qoni Console/SDK gateway override for private or local deployments. |
|
|
514
|
+
| `fetch` | `typeof fetch` | No | Custom transport implementation. |
|
|
515
|
+
| `timeoutMs` | `number` | No | Per-request timeout (connect + response headers). Defaults to `30000`. |
|
|
516
|
+
|
|
517
|
+
> **`timeoutMs` does not bound a long-running run.** It applies to individual
|
|
518
|
+
> one-shot HTTP requests (delegation, `status()`, a single POST). The live
|
|
519
|
+
> event stream (`events()` / `wait()`) is **not** cut off by it — once the
|
|
520
|
+
> stream's response headers arrive the per-request timer is cleared, so a
|
|
521
|
+
> Deep Research run that takes an hour streams fine under the default 30s
|
|
522
|
+
> client timeout. To bound how long you wait, pass `wait({ timeoutMs })` —
|
|
523
|
+
> with no value, `wait()` runs until the run reaches a terminal state. A
|
|
524
|
+
> `wait()` timeout does not stop the run; it keeps executing server-side and
|
|
525
|
+
> you can `attach()` to it again.
|
|
526
|
+
|
|
527
|
+
`Qoni` is also exported as a short alias:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { Qoni, QoniAgentKit } from "@qoniai/qoni";
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Namespaces
|
|
534
|
+
|
|
535
|
+
| Namespace | Methods |
|
|
536
|
+
| --- | --- |
|
|
537
|
+
| Delegation | `delegateToken`, `completeDelegateToken`, deprecated aliases `delegateAgent`, `completeDelegateAgent` |
|
|
538
|
+
| GenAuth | `userInfo`, `jwks`, `discovery`, `introspectDelegationToken`, `users.list`, `users.get`, `users.getBatch`, `users.create`, `users.createBatch`, `users.update`, `users.deleteBatch` |
|
|
539
|
+
| GUMem | `createSession`, `addMessages`, `recall`, `uploadResource`, `actions.record`, `actions.recall`, `actions.stream` |
|
|
540
|
+
| Do Anything | `run`, `attach` → `RunHandle` |
|
|
541
|
+
| Deep Research | `run`, `attach` → `RunHandle` |
|
|
542
|
+
| Web Search | `run`, `attach` → `RunHandle` |
|
|
543
|
+
| Track | `create`, `attach` → `MonitorHandle` |
|
|
544
|
+
|
|
545
|
+
The three run products share one handle shape:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
run.id
|
|
549
|
+
run.sessionRef // pass back to run({ session }) to reuse the session
|
|
550
|
+
await run.status() // refresh and return the current state
|
|
551
|
+
run.events({ onWireEvent?, onReconnect?, ...opts }) // semantic events + optional raw telemetry
|
|
552
|
+
await run.wait({ timeoutMs?, onEvent?, onWireEvent?, onReconnect?, onInteraction?, onScreenshot? })
|
|
553
|
+
run.interactionHandle(interaction) // typed HITL handle (see Interactions below)
|
|
554
|
+
await run.cancel(reason?) // idempotent
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
`await run.wait(...)` settles to a `RunResult`:
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
interface RunResult {
|
|
561
|
+
runId: string;
|
|
562
|
+
status: "succeeded" | "failed" | "canceled"; // delivery status of the run
|
|
563
|
+
output?: unknown; // final answer / payload (shape is task-specific)
|
|
564
|
+
artifacts: Artifact[]; // deliverables — deepResearch reports; [] for the others
|
|
565
|
+
isTaskSuccessful?: boolean; // did the task achieve its goal (distinct from `status`)
|
|
566
|
+
terminalReason?: string;
|
|
567
|
+
raw: JsonObject; // full wire envelope — cost, step counts, token usage
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Each `Artifact` exposes `id`, `name?`, `mime?`, `sizeBytes?`, `createdAt?`, and
|
|
572
|
+
`content(): Promise<Uint8Array>` (lazy byte download). Every other method returns the `QoniResponse<T>` envelope below; `T`
|
|
573
|
+
is the typed payload for that call (e.g. `genauth.users.list` → a paged user
|
|
574
|
+
list on `.data`). The exact `T` shapes live in the bundled `.d.ts` and follow
|
|
575
|
+
the backend contract.
|
|
576
|
+
|
|
577
|
+
`MonitorHandle` (Track) exposes `id`, `get()`, `pause()`, `resume()`, `refine(patch)`, `runNow()`, `events()`, `interactionHandle()`, `runs()`, `run(runId)`, and `delete()`. `runNow()` and `refine()` require the monitor to be `active` — a freshly created monitor is briefly `pending_clarification` first (see the Track example).
|
|
578
|
+
|
|
579
|
+
### Interactions (HITL)
|
|
580
|
+
|
|
581
|
+
When a run needs the user — sign in to a site, answer a clarification, approve a
|
|
582
|
+
plan, take over the browser, or wait — it emits an `interaction` event whose
|
|
583
|
+
`event.data` is a typed `Interaction` (discriminated by `type`: `site_login` /
|
|
584
|
+
`clarification` / `confirmation` / `take_control` / `wait`). Wrap it in a handle
|
|
585
|
+
and call the action methods; each posts to the endpoint the backend declared in
|
|
586
|
+
the interaction's `actions[]`, so you never hard-code a route:
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
for await (const event of run.events()) {
|
|
590
|
+
if (event.type === "interaction") {
|
|
591
|
+
const i = run.interactionHandle(event.data); // event.data is the Interaction
|
|
592
|
+
if (i.type === "clarification" && i.can("answer")) await i.answer("the blue button");
|
|
593
|
+
else if (i.can("confirm")) await i.confirm();
|
|
594
|
+
else if (i.can("confirm_signed_in")) await i.confirmSignedIn();
|
|
595
|
+
else if (i.can("release_control")) await i.releaseControl();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin()`,
|
|
601
|
+
`confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
|
|
602
|
+
`retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
|
|
603
|
+
currently offers that action; calling a method the backend did NOT declare
|
|
604
|
+
throws (so a "recovery" button only exists when it actually works).
|
|
605
|
+
|
|
606
|
+
The discriminant strings used above are also exported as constants so you can
|
|
607
|
+
avoid bare literals: `InteractionTypes` (`site_login` / `clarification` /
|
|
608
|
+
`confirmation` / `take_control` / `wait`), `InteractionStatuses`, and
|
|
609
|
+
`ActionKinds` (the `i.can(...)` action names). Authorization scope strings have
|
|
610
|
+
the same treatment — `QoniScopes` (every individual scope), `QoniProductScopes`
|
|
611
|
+
(the per-product `read`+`manage` pairs the `products` sugar expands to), and
|
|
612
|
+
`QoniScopeBundles` (curated bundles). Use them in place of hand-typed strings:
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
|
|
616
|
+
|
|
617
|
+
if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
|
|
618
|
+
await i.answer("the blue button");
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Wire-Level Escape Hatch
|
|
623
|
+
|
|
624
|
+
The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnything.api.createSession`, `qoni.deepResearch.api.followUp`, `qoni.track.api.createMonitor`). These mirror the backend HTTP contract directly — snake_case fields, double-envelope events, single-ID `/{product}/runs/{run_id}` addressing for run operations — and their shapes may evolve with the API. They are not part of the frozen public contract.
|
|
625
|
+
|
|
626
|
+
`event.raw.event` is the **raw wire event type** — an internal transport name (`run.action.started`, `run.section_completed`, …) whose naming is historical and inconsistent, exposed as a plain string. This is an implementation detail, not the product API: developers match on the curated `event.type` above. The full wire-type catalog is intentionally NOT exported — reach for `event.raw.event` (a string) only when you need an exact wire match the curated layer folds into `progress` (e.g. telling a specific browser action apart).
|
|
627
|
+
|
|
628
|
+
Browser Use and Site Login scopes are reserved until their product runtime SDK methods are exported.
|
|
629
|
+
|
|
630
|
+
### Response Shape
|
|
631
|
+
|
|
632
|
+
Most SDK methods return:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
type QoniResponse<T> = {
|
|
636
|
+
data: T;
|
|
637
|
+
meta: {
|
|
638
|
+
requestId?: string;
|
|
639
|
+
traceId?: string;
|
|
640
|
+
auditId?: string;
|
|
641
|
+
service?: "qoni" | "genauth" | "gumem" | "webagent";
|
|
642
|
+
};
|
|
643
|
+
};
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Handle event streams (`run.events()`, `monitor.events()`) yield a discriminated
|
|
647
|
+
`RunEvent` — once you match `event.type`, `event.data` is the fixed shape for
|
|
648
|
+
that type:
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
// event.type → event.data:
|
|
652
|
+
// SIMPLE events — event.data IS the value (no field to remember):
|
|
653
|
+
// "progress" string — a human-readable line of what's happening
|
|
654
|
+
// "phase" string — the Deep Research phase name
|
|
655
|
+
// "sectionReady" string — the Deep Research section title
|
|
656
|
+
// "resultsReady" number — Web Search count of unique results
|
|
657
|
+
// "monitorCreated" string — Track monitor id
|
|
658
|
+
// "triggered" string — Track change summary
|
|
659
|
+
// "checkCompleted" boolean — Track: did the scheduled check find a change
|
|
660
|
+
// RICH events — event.data is a small object:
|
|
661
|
+
// "message" { text, role }
|
|
662
|
+
// "interaction" the typed Interaction object (HITL) — see Interactions
|
|
663
|
+
// "screenshot" { pageUrl?, step? } + the decoded image on event.image
|
|
664
|
+
// "done" { output, succeeded, terminalReason }
|
|
665
|
+
// Common to every event: runId, at (ISO 8601), isTerminal (true only for "done"),
|
|
666
|
+
// raw (the original wire event — escape hatch).
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Each product handle is typed to its own subset:
|
|
670
|
+
|
|
671
|
+
| Product | `event.type` set |
|
|
672
|
+
|---|---|
|
|
673
|
+
| Do Anything | core: `progress` / `message` / `interaction` / `screenshot` / `done` |
|
|
674
|
+
| Web Search | core + `resultsReady` |
|
|
675
|
+
| Deep Research | core + `phase` / `sectionReady` (the outline gate arrives as an `interaction`) |
|
|
676
|
+
| Track (`MonitorHandle`) | core + `monitorCreated` / `triggered` / `checkCompleted` |
|
|
677
|
+
|
|
678
|
+
Dropped connections reconnect automatically with `Last-Event-ID` catch-up; the iterator ends at the run's terminal (`done`) event.
|
|
679
|
+
For complete audit/observability capture, `onWireEvent` receives every original
|
|
680
|
+
SSE frame before semantic normalization or filtering; `onReconnect` receives
|
|
681
|
+
the retry attempt and last event id. Keep application behavior on the curated
|
|
682
|
+
`RunEvent` stream and use the wire callback only as evidence/diagnostics.
|
|
683
|
+
|
|
684
|
+
Wire-level streaming methods (`qoni.<product>.api.events`) return `AsyncIterable<QoniEvent<T>>`:
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
type QoniEvent<T = unknown> = {
|
|
688
|
+
id?: string;
|
|
689
|
+
event?: string;
|
|
690
|
+
data: T;
|
|
691
|
+
};
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## Error Handling
|
|
695
|
+
|
|
696
|
+
All SDK errors inherit from `QoniError` and expose `code`, `status`, `requestId`, `traceId`, `auditId`, `retryable`, and the original response `body` when available. `code` is typed as `QoniErrorCode` (the known SDK / backend codes plus `(string & {})`), so you get literal autocomplete and can `switch (err.code)` over the known set while still tolerating new backend codes.
|
|
697
|
+
|
|
698
|
+
All error classes are exported and can be matched with `instanceof`:
|
|
699
|
+
|
|
700
|
+
| Class | Fires when | Typical recovery |
|
|
701
|
+
| --- | --- | --- |
|
|
702
|
+
| `QoniValidationError` | Local input check failed before any request (bad scope string, silent delegation missing a user). | Fix the call — the message states the correct form. |
|
|
703
|
+
| `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
|
|
704
|
+
| `QoniPermissionDeniedError` | The delegation token lacks a required scope (403). | Re-`delegateToken` with the missing scope. |
|
|
705
|
+
| `QoniTokenExpiredError` | The delegation / product token expired. | Mint a fresh token and retry. |
|
|
706
|
+
| `QoniDelegationRequiredError` | A product call was made without `token`, or with a raw token the gateway won't accept directly. | Pass `delegation.data.token` to the product call. |
|
|
707
|
+
| `QoniRateLimitError` | Rate limited (429). | Back off and retry (`retryable === true`). |
|
|
708
|
+
| `QoniTimeoutError` | A request (or `wait({ timeoutMs })`) timed out. | Retry; the run keeps executing server-side — `attach()` to it. |
|
|
709
|
+
| `QoniUpstreamError` | A downstream service failed (5xx / upstream exchange failure). | Back off and retry when `retryable`. |
|
|
710
|
+
| `QoniError` | Base class / any other code. | Inspect `err.code`. |
|
|
711
|
+
|
|
712
|
+
The SDK does **not** auto-retry. `err.retryable` tells you whether a retry could
|
|
713
|
+
succeed; you own the backoff.
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
import {
|
|
717
|
+
QoniDelegationRequiredError,
|
|
718
|
+
QoniPermissionDeniedError,
|
|
719
|
+
QoniRateLimitError,
|
|
720
|
+
QoniTimeoutError,
|
|
721
|
+
QoniTokenExpiredError,
|
|
722
|
+
QoniUpstreamError,
|
|
723
|
+
} from "@qoniai/qoni";
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
await qoni.webSearch.run({ token, prompt: "Qoni SDK" });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (
|
|
729
|
+
error instanceof QoniPermissionDeniedError ||
|
|
730
|
+
error instanceof QoniTokenExpiredError ||
|
|
731
|
+
error instanceof QoniDelegationRequiredError
|
|
732
|
+
) {
|
|
733
|
+
// Re-mint a delegation token (add the missing scope / refresh expiry) and retry.
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
error instanceof QoniRateLimitError ||
|
|
738
|
+
error instanceof QoniTimeoutError ||
|
|
739
|
+
error instanceof QoniUpstreamError
|
|
740
|
+
) {
|
|
741
|
+
// Transient — back off and retry when `error.retryable` is true.
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Common first-run errors:
|
|
749
|
+
|
|
750
|
+
| Error | Meaning | Next step |
|
|
751
|
+
| --- | --- | --- |
|
|
752
|
+
| `agent must be a string` | The online delegation API currently expects `agent` to be a string id. | Pass `agent: "memory-agent"` rather than an object. |
|
|
753
|
+
| `eak.delegation.user_not_bound` | The `userId` is not in the GenAuth userpool bound to this Qoni credential. | Use a real user id from `currentUser` or from the same bound userpool. |
|
|
754
|
+
| `eak.genauth.userpool_binding_missing` | The AK/SK is not bound to a GenAuth userpool, so `genauth.users.*` cannot obtain a management token. | Bind the Qoni credential to the target GenAuth userpool before calling user management APIs. |
|
|
755
|
+
| `eak.genauth.userpool_owner_missing` | The bound GenAuth userpool has no resolvable owner user for management-token signing. | Fix the GenAuth userpool owner data or binding. |
|
|
756
|
+
| `eak.token_exchange.upstream_failed` with `unauthorized_client` or `grant_type is not enabled` | Delegation succeeded, but the managed delegation app is missing token-exchange grant support or has drifted from the tenant binding. | Repair the managed delegation app binding before calling GUMem or WebAgent product capabilities. |
|
|
757
|
+
| `delegation.required` | A GUMem/WebAgent call was made without `token`. | Pass `delegation.data.token` to the product namespace call. |
|
|
758
|
+
| `direct delegation token deprecated` | Application code is calling a product service directly with an Qoni delegation token. | Call the product through the SDK so token exchange is handled internally. |
|
|
759
|
+
|
|
760
|
+
## Security Notes
|
|
761
|
+
|
|
762
|
+
- Keep Qoni AK/SK on trusted servers only.
|
|
763
|
+
- Do not expose delegation tokens to untrusted clients.
|
|
764
|
+
- Request the smallest useful scope set for each Agent action.
|
|
765
|
+
- Prefer `interactive` mode for browser control, site login, long-running monitors, and sensitive artifacts.
|
|
766
|
+
- Store `auditId`, `requestId`, and `traceId` with product logs when later investigation matters.
|
|
767
|
+
- Rotate AK/SK through Qoni Console if a credential might have been exposed.
|
|
768
|
+
|
|
769
|
+
## License
|
|
770
|
+
|
|
771
|
+
MIT
|