@ouispec/agent-realtime 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/approvals/client-event.d.ts +4 -0
- package/dist/approvals/client-event.d.ts.map +1 -0
- package/dist/approvals/client-event.js +25 -0
- package/dist/approvals/client-event.js.map +1 -0
- package/dist/approvals/routes.d.ts +19 -0
- package/dist/approvals/routes.d.ts.map +1 -0
- package/dist/approvals/routes.js +100 -0
- package/dist/approvals/routes.js.map +1 -0
- package/dist/approvals/schemas.d.ts +7 -0
- package/dist/approvals/schemas.d.ts.map +1 -0
- package/dist/approvals/schemas.js +45 -0
- package/dist/approvals/schemas.js.map +1 -0
- package/dist/approvals/store.d.ts +33 -0
- package/dist/approvals/store.d.ts.map +1 -0
- package/dist/approvals/store.js +206 -0
- package/dist/approvals/store.js.map +1 -0
- package/dist/approvals/token.d.ts +19 -0
- package/dist/approvals/token.d.ts.map +1 -0
- package/dist/approvals/token.js +73 -0
- package/dist/approvals/token.js.map +1 -0
- package/dist/client-events/builtin.d.ts +30 -0
- package/dist/client-events/builtin.d.ts.map +1 -0
- package/dist/client-events/builtin.js +161 -0
- package/dist/client-events/builtin.js.map +1 -0
- package/dist/client-events/chain.d.ts +80 -0
- package/dist/client-events/chain.d.ts.map +1 -0
- package/dist/client-events/chain.js +92 -0
- package/dist/client-events/chain.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/events/declared.d.ts +18 -0
- package/dist/events/declared.d.ts.map +1 -0
- package/dist/events/declared.js +30 -0
- package/dist/events/declared.js.map +1 -0
- package/dist/events/settlements.d.ts +28 -0
- package/dist/events/settlements.d.ts.map +1 -0
- package/dist/events/settlements.js +29 -0
- package/dist/events/settlements.js.map +1 -0
- package/dist/http/routes.d.ts +91 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +290 -0
- package/dist/http/routes.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +22 -0
- package/dist/logger.js.map +1 -0
- package/dist/oui/results.d.ts +46 -0
- package/dist/oui/results.d.ts.map +1 -0
- package/dist/oui/results.js +56 -0
- package/dist/oui/results.js.map +1 -0
- package/dist/redis-waits.d.ts +32 -0
- package/dist/redis-waits.d.ts.map +1 -0
- package/dist/redis-waits.js +62 -0
- package/dist/redis-waits.js.map +1 -0
- package/dist/rooms/join.d.ts +13 -0
- package/dist/rooms/join.d.ts.map +1 -0
- package/dist/rooms/join.js +27 -0
- package/dist/rooms/join.js.map +1 -0
- package/dist/rooms/room-token.d.ts +19 -0
- package/dist/rooms/room-token.d.ts.map +1 -0
- package/dist/rooms/room-token.js +55 -0
- package/dist/rooms/room-token.js.map +1 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +168 -0
- package/dist/server.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +87 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Closure Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @ouispec/agent-realtime
|
|
2
|
+
|
|
3
|
+
The realtime server for agent turns and UI actions. One implementation serves every product (ADR-0227). It does six things:
|
|
4
|
+
|
|
5
|
+
- **Carries events.** Turn events, UI action dispatches and the product's own events reach rooms through the internal emit API.
|
|
6
|
+
- **Holds UI action results.** It keeps each browser's answer to a UI action for the agent worker that asked (ADR-0209). Answers are shared across instances through Redis.
|
|
7
|
+
- **Keeps approvals.** An irreversible action runs only on an approval the user gave, bound to the exact call by a single-use token (ADR-0228).
|
|
8
|
+
- **Signs and checks room tokens.** A room token admits one user to one room.
|
|
9
|
+
- **Checks client events.** It accepts only the client events it declares, and puts each one through a chain: rate limit, size cap, schema, then authorization against the verified identity.
|
|
10
|
+
- **Restricts relays.** A client can relay only allow-listed events, and only into rooms its socket has joined.
|
|
11
|
+
|
|
12
|
+
## What the product supplies
|
|
13
|
+
|
|
14
|
+
Every seam is required. When one is missing, the server fails at start-up and names it. No seam defaults to another product's value.
|
|
15
|
+
|
|
16
|
+
| Seam | Config | What it is |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Auth verifier | `auth` | `AuthAdapter.verify(token)`. It verifies your users' tokens once per socket and returns `{ userId, accountId?, platformAdmin?, metadata? }`. |
|
|
19
|
+
| Room policy | `roomPolicy` | `isValidRoom`, `identityRooms` (the rooms a socket joins on connect and can never leave), `requiresToken`, and `canJoin` for the rooms that need no token. |
|
|
20
|
+
| Internal key | `internalApiKey` | Guards `/api/emit` and `/internal/*`. Use one per environment, kept in your secret store. |
|
|
21
|
+
| Room-token secret | `roomTokens.secret` | Signs room tokens with HMAC-SHA256. It must be at least 32 characters and must differ from the internal key. `ttlMs` is optional and defaults to 15 minutes. |
|
|
22
|
+
| Approval signing key | `approvals.signingKey` | Signs approval tokens (compact JWS, HS256). It must be at least 32 characters and differ from the internal key and the room-token secret. Use one per environment; an engine that verifies approval tokens holds this key and nothing else. |
|
|
23
|
+
| Redis | `redis` | `{ host, port, tls }`. UI action results and room fan-out cross instances here. |
|
|
24
|
+
| CORS origins | `corsOrigins` | The browser origins allowed to connect. Pass `['*']` only on purpose. |
|
|
25
|
+
| Port | `port` | The port to listen on. `0` picks a free one. |
|
|
26
|
+
|
|
27
|
+
Optional:
|
|
28
|
+
- `events`: your event declarations, with the platform's own: `createEventCatalog(PLATFORM_EVENTS, yourEvents)` from `@ouispec/agent-events` (see below).
|
|
29
|
+
- `relay`: the client-relayable events, each with a room prefix and a data schema. With `events`, the declaration supplies both, so `{}` is enough; the event must be a declared `notice` that does not go to a turn's room.
|
|
30
|
+
- `clientEvents`: your own declared events, built with `defineClientEvent`.
|
|
31
|
+
- `logger`: a pino-shaped logger.
|
|
32
|
+
- `pingInterval` and `pingTimeout`.
|
|
33
|
+
|
|
34
|
+
## Room tokens: the product decides, the server signs
|
|
35
|
+
|
|
36
|
+
A room that `requiresToken` is joined only with a token binding this user to this exact room. Minting one takes two steps:
|
|
37
|
+
|
|
38
|
+
1. The browser asks your API for a token. Your API runs the resource's own read authorization, the same check its GET route applies.
|
|
39
|
+
2. Only if that check passes does your API call `POST /internal/room-token` with `{ userId, room }` and the internal key. The response is `{ token, expiresInMs }`, and your API returns the token to the browser.
|
|
40
|
+
|
|
41
|
+
The browser then joins with `socket.emit('subscribe', { rooms: [room], tokens: { [room]: token } }, ack)`. A room joined by identity is refused a token.
|
|
42
|
+
|
|
43
|
+
## Declared events
|
|
44
|
+
|
|
45
|
+
Pass `events` and the server follows your declarations (ADR-0227 §2.4):
|
|
46
|
+
|
|
47
|
+
- **Emit.** `/api/emit` sends only a declared event, with a payload its schema accepts, into rooms it is declared for. Anything else is refused with `400 { error }` and logged as `Emit refused` with the event, the rooms and the reason. A declared event is never broadcast.
|
|
48
|
+
- **Relay.** A client relays only an event you list in `relay` that is declared as a `notice`. Its declaration decides the rooms and the data it may carry. Listing a completion, failure, progress or turn event fails at start-up.
|
|
49
|
+
- **Settlements.** Every declared completion or failure you emit settles one job: the job of its kind whose id is in its correlation field. The first is kept for 35 minutes, shared across instances, so a worker can wait for a job to end without joining a room (`createHttpEventWaiter` in `@ouispec/agent-worker`).
|
|
50
|
+
|
|
51
|
+
The platform's own events (the worker's turn events and OUI's UI action dispatch) are declared in `PLATFORM_EVENTS`, so they pass the same checks.
|
|
52
|
+
|
|
53
|
+
## Internal endpoints
|
|
54
|
+
|
|
55
|
+
All of them require the `x-api-key` header.
|
|
56
|
+
|
|
57
|
+
| Endpoint | Purpose |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `POST /api/emit` `{ event, data, rooms? }` | Sends a server event to rooms. With no `rooms`, it goes to every socket. |
|
|
60
|
+
| `POST /internal/room-token` `{ userId, room }` | Signs a room token (see above). |
|
|
61
|
+
| `GET /internal/oui/action-results/:requestId?userId=&waitMs=&final=1` | Returns the browser's answer to a UI action. The first answer wins. With `final=1` you get an async action's final answer instead. Answers are returned only to the turn's own user. `200` carries the result; `204` means not yet, so ask again. A wait lasts at most 25 s. |
|
|
62
|
+
| `POST /internal/approvals` | Stores a pending approval (the worker, or a conversation engine). `201`; `400` for a bad body, a hash that does not match the arguments, or an expiry more than 30 minutes away; `409` for an id already stored. |
|
|
63
|
+
| `POST /internal/approvals/:id/decide` `{ userId, decision, channel }` | A conversation channel's decision (`voice`, `phone`, `sms`, `chat`), made by the engine for the session's user. A `ui` decision is refused here: in a UI only the user's click counts. |
|
|
64
|
+
| `POST /internal/approvals/redeem` `{ token, userId, conversationId }` | Atomic single use. `200 { call }` returns exactly the stored call. `403` for a token this environment did not sign, another user's or conversation's, or one not for the stored call; `410` for an expired or already used one. |
|
|
65
|
+
| `GET /internal/approvals/:id?userId=` | Where an approval of this user's stands: `pending`, `approved` or `declined` (remembered for 30 minutes). |
|
|
66
|
+
| `GET /internal/events/settlements/:kind/:id?waitMs=` | How a job of a declared kind ended: `{ kind, role, event, id, payload }`, with `role` `completion` or `failure`. `204` means not yet; `400` an undeclared kind; `404` a server without `events`. A wait lasts at most 25 s. |
|
|
67
|
+
| `GET /health` | Returns `200` with connection and pending-wait counts. No key needed. |
|
|
68
|
+
|
|
69
|
+
## Approvals (ADR-0228)
|
|
70
|
+
|
|
71
|
+
1. The worker stops the turn at a call that needs approval, stores it with `POST /internal/approvals`, and emits `agent:approval_required` with the preview.
|
|
72
|
+
2. The user clicks the approval card. Their socket sends `approval:decide { approvalId, decision }`, a declared client event. The store checks that the socket's verified user is the approval's own. An approval's token is answered only to that socket, in the ack: `{ ok: true, decision: 'approve', token, argsHash, expiresAt }`.
|
|
73
|
+
3. The next turn carries the token outside the message text. The worker redeems it, and the store returns the stored call exactly once.
|
|
74
|
+
|
|
75
|
+
A declined approval is deleted and remembered for the next turn. Every issue, decision and redemption is logged with the approval id, user, tool, effect, channel and args hash. The arguments are logged only when their declaration marks them not sensitive, and a token never is.
|
|
76
|
+
|
|
77
|
+
The token's claims are `aid sub cid tool ah eff ch iat exp jti`. `ah` is `argsHash(args)` from `@ouispec/agent-core`: SHA-256 over the RFC 8785 canonical JSON. The package ships `approval-vectors.json`, which every other implementation must match.
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createRealtimeServer } from '@ouispec/agent-realtime';
|
|
83
|
+
|
|
84
|
+
const server = await createRealtimeServer({
|
|
85
|
+
auth: { verify: (token) => verifyMyUser(token) },
|
|
86
|
+
roomPolicy: {
|
|
87
|
+
isValidRoom: (room) => /^member:[\w-]+$|^chat:turn:[\w-]+$/.test(room),
|
|
88
|
+
identityRooms: (user) => [`member:${user.userId}`],
|
|
89
|
+
requiresToken: (room) => room.startsWith('chat:turn:'),
|
|
90
|
+
canJoin: (room, user) => room === `member:${user.userId}`,
|
|
91
|
+
},
|
|
92
|
+
internalApiKey: secrets.realtimeInternalKey,
|
|
93
|
+
roomTokens: { secret: secrets.roomTokenSecret },
|
|
94
|
+
approvals: { signingKey: secrets.approvalSigningKey },
|
|
95
|
+
redis: { host: env.REDIS_HOST, port: 6379, tls: true },
|
|
96
|
+
corsOrigins: ['https://app.example.com'],
|
|
97
|
+
port: 3331,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
`@ouispec/agent-realtime/testing` exports `startTestRedis()`. If `REDIS_URL` is set it uses that Redis. Otherwise it starts a private `redis-server` on a free port. If neither is available, it fails.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-event.d.ts","sourceRoot":"","sources":["../../src/approvals/client-event.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,aAAa,GAAG,WAAW,CAcrF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approval:decide` (ADR-0228 §2.2.3): the user's click on the approval card,
|
|
3
|
+
* on their own authenticated socket. The decider is the socket's verified
|
|
4
|
+
* identity, never one the payload names, and an approval's token is answered
|
|
5
|
+
* only to this socket, in the event's ack.
|
|
6
|
+
*/
|
|
7
|
+
import { APPROVAL_DECIDE_EVENT } from '@ouispec/agent-core';
|
|
8
|
+
import { defineClientEvent } from '../client-events/chain.js';
|
|
9
|
+
import { decidePayloadSchema } from './schemas.js';
|
|
10
|
+
export function createApprovalDecideEvent(approvals) {
|
|
11
|
+
return defineClientEvent({
|
|
12
|
+
name: APPROVAL_DECIDE_EVENT,
|
|
13
|
+
schema: decidePayloadSchema,
|
|
14
|
+
maxBytes: 1024,
|
|
15
|
+
// A person clicks; a burst is a script.
|
|
16
|
+
rate: { perSecond: 2, burst: 5 },
|
|
17
|
+
// Scoped by construction: the store decides only an approval whose user is this socket's.
|
|
18
|
+
refusal: (reason) => ({ ok: false, reason: 'invalid', error: reason }),
|
|
19
|
+
handle: async ({ user }, payload, ack) => {
|
|
20
|
+
const result = await approvals().decide(payload.approvalId, { userId: user.userId, decision: payload.decision, channel: 'ui' });
|
|
21
|
+
ack?.(result);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=client-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-event.js","sourceRoot":"","sources":["../../src/approvals/client-event.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAoD,MAAM,qBAAqB,CAAC;AAC9G,OAAO,EAAE,iBAAiB,EAAoB,MAAM,2BAA2B,CAAC;AAEhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,UAAU,yBAAyB,CAAC,SAA8B;IACtE,OAAO,iBAAiB,CAAwB;QAC9C,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,mBAAmB;QAC3B,QAAQ,EAAE,IAAI;QACd,wCAAwC;QACxC,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;QAChC,0FAA0F;QAC1F,OAAO,EAAE,CAAC,MAAM,EAAmB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACvF,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAChI,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QAChB,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval store's internal endpoints (ADR-0228 §2.4). Each requires the
|
|
3
|
+
* internal key: they are for the product's agent worker and a conversation
|
|
4
|
+
* engine, never a browser. A browser decides only through `approval:decide`
|
|
5
|
+
* on its own authenticated socket.
|
|
6
|
+
*
|
|
7
|
+
* POST /internal/approvals store a pending approval (worker, engine)
|
|
8
|
+
* POST /internal/approvals/:id/decide a conversation channel's decision (engine)
|
|
9
|
+
* POST /internal/approvals/redeem atomic single use (worker, engine)
|
|
10
|
+
* GET /internal/approvals/:id?userId= where an approval stands (worker, for the turn after a decision)
|
|
11
|
+
*/
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
import type { ApprovalStore } from './store.js';
|
|
14
|
+
export interface ApprovalRouteDeps {
|
|
15
|
+
approvals: ApprovalStore;
|
|
16
|
+
isInternalKey: (presented: string | undefined) => boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function approvalRouter(deps: ApprovalRouteDeps): Router;
|
|
19
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/approvals/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAA+B,MAAM,SAAS,CAAC;AAG9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,aAAa,CAAC;IACzB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC;CAC3D;AAkBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAyE9D"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval store's internal endpoints (ADR-0228 §2.4). Each requires the
|
|
3
|
+
* internal key: they are for the product's agent worker and a conversation
|
|
4
|
+
* engine, never a browser. A browser decides only through `approval:decide`
|
|
5
|
+
* on its own authenticated socket.
|
|
6
|
+
*
|
|
7
|
+
* POST /internal/approvals store a pending approval (worker, engine)
|
|
8
|
+
* POST /internal/approvals/:id/decide a conversation channel's decision (engine)
|
|
9
|
+
* POST /internal/approvals/redeem atomic single use (worker, engine)
|
|
10
|
+
* GET /internal/approvals/:id?userId= where an approval stands (worker, for the turn after a decision)
|
|
11
|
+
*/
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
import { internalDecideSchema, pendingApprovalSchema, redeemSchema } from './schemas.js';
|
|
14
|
+
const DECIDE_STATUS = {
|
|
15
|
+
unknown: 404,
|
|
16
|
+
forbidden: 403,
|
|
17
|
+
expired: 410,
|
|
18
|
+
decided: 409,
|
|
19
|
+
used: 410,
|
|
20
|
+
invalid: 400,
|
|
21
|
+
mismatch: 403,
|
|
22
|
+
channel: 400,
|
|
23
|
+
};
|
|
24
|
+
const REDEEM_STATUS = {
|
|
25
|
+
...DECIDE_STATUS,
|
|
26
|
+
invalid: 403,
|
|
27
|
+
unknown: 410,
|
|
28
|
+
};
|
|
29
|
+
export function approvalRouter(deps) {
|
|
30
|
+
const router = Router();
|
|
31
|
+
const accept = (req, res, schema) => {
|
|
32
|
+
if (!deps.isInternalKey(req.headers['x-api-key'])) {
|
|
33
|
+
res.status(401).json({ error: 'Invalid or missing API key' });
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const { value, error } = schema.validate(req.body ?? {}, { abortEarly: true, convert: false });
|
|
37
|
+
if (error) {
|
|
38
|
+
res.status(400).json({ error: `invalid body: ${error.message}`, reason: 'invalid' });
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
router.post('/internal/approvals/redeem', async (req, res) => {
|
|
44
|
+
const body = accept(req, res, redeemSchema);
|
|
45
|
+
if (!body)
|
|
46
|
+
return;
|
|
47
|
+
const result = await deps.approvals.redeem(body.token, { userId: body.userId, conversationId: body.conversationId });
|
|
48
|
+
if (!result.ok) {
|
|
49
|
+
res.status(REDEEM_STATUS[result.reason]).json({ error: result.error, reason: result.reason });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
res.json({ call: result.call });
|
|
53
|
+
});
|
|
54
|
+
router.post('/internal/approvals', async (req, res) => {
|
|
55
|
+
const body = accept(req, res, pendingApprovalSchema);
|
|
56
|
+
if (!body)
|
|
57
|
+
return;
|
|
58
|
+
const result = await deps.approvals.create(body);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
res.status(result.reason === 'decided' ? 409 : 400).json({ error: result.error, reason: result.reason });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.status(201).json({ approvalId: result.approvalId, argsHash: result.argsHash, expiresAt: result.expiresAt });
|
|
64
|
+
});
|
|
65
|
+
router.post('/internal/approvals/:id/decide', async (req, res) => {
|
|
66
|
+
const body = accept(req, res, internalDecideSchema);
|
|
67
|
+
if (!body)
|
|
68
|
+
return;
|
|
69
|
+
// In a UI only the user's click counts, and it arrives on their own socket.
|
|
70
|
+
if (body.channel === 'ui') {
|
|
71
|
+
res.status(400).json({ error: 'a UI approval is decided only on the user’s own socket (approval:decide)', reason: 'channel' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = await deps.approvals.decide(req.params.id, body);
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
res.status(DECIDE_STATUS[result.reason]).json({ error: result.error, reason: result.reason });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.json(result);
|
|
80
|
+
});
|
|
81
|
+
router.get('/internal/approvals/:id', async (req, res) => {
|
|
82
|
+
if (!deps.isInternalKey(req.headers['x-api-key'])) {
|
|
83
|
+
res.status(401).json({ error: 'Invalid or missing API key' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const userId = typeof req.query.userId === 'string' ? req.query.userId : '';
|
|
87
|
+
if (!userId) {
|
|
88
|
+
res.status(400).json({ error: 'userId is required' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const status = await deps.approvals.status(req.params.id, userId);
|
|
92
|
+
if (!status) {
|
|
93
|
+
res.status(404).json({ error: 'no such approval for this user', reason: 'unknown' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
res.json(status);
|
|
97
|
+
});
|
|
98
|
+
return router;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/approvals/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The shapes the approval endpoints and the `approval:decide` event accept. Unknown keys are refused. */
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
export declare const pendingApprovalSchema: Joi.ObjectSchema<any>;
|
|
4
|
+
export declare const decidePayloadSchema: Joi.ObjectSchema<any>;
|
|
5
|
+
export declare const internalDecideSchema: Joi.ObjectSchema<any>;
|
|
6
|
+
export declare const redeemSchema: Joi.ObjectSchema<any>;
|
|
7
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/approvals/schemas.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,OAAO,GAAG,MAAM,KAAK,CAAC;AAMtB,eAAO,MAAM,qBAAqB,uBAsBhC,CAAC;AAEH,eAAO,MAAM,mBAAmB,uBAG9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,uBAM/B,CAAC;AAEH,eAAO,MAAM,YAAY,uBAIvB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** The shapes the approval endpoints and the `approval:decide` event accept. Unknown keys are refused. */
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
import { APPROVAL_CHANNELS, ARGS_HASH_PATTERN } from '@ouispec/agent-core';
|
|
4
|
+
const id = Joi.string().min(1).max(200);
|
|
5
|
+
const text = (max) => Joi.string().max(max);
|
|
6
|
+
export const pendingApprovalSchema = Joi.object({
|
|
7
|
+
approvalId: id.required(),
|
|
8
|
+
toolCallId: id.required(),
|
|
9
|
+
conversationId: id.required(),
|
|
10
|
+
turnId: id.required(),
|
|
11
|
+
userId: id.required(),
|
|
12
|
+
tool: id.required(),
|
|
13
|
+
args: Joi.object().unknown(true).required(),
|
|
14
|
+
argsHash: Joi.string().pattern(ARGS_HASH_PATTERN).required(),
|
|
15
|
+
effect: Joi.string().min(1).max(64).required(),
|
|
16
|
+
destructive: Joi.boolean().required(),
|
|
17
|
+
argsSensitive: Joi.boolean().required(),
|
|
18
|
+
expiresAt: Joi.number().integer().positive().required(),
|
|
19
|
+
preview: Joi.object({
|
|
20
|
+
title: text(500).min(1).required(),
|
|
21
|
+
consequence: text(2000),
|
|
22
|
+
arguments: Joi.array()
|
|
23
|
+
.items(Joi.object({ name: text(200).required(), label: text(500).required(), value: text(4000).allow('').required() }))
|
|
24
|
+
.max(50)
|
|
25
|
+
.required(),
|
|
26
|
+
readback: text(8000).allow('').required(),
|
|
27
|
+
}).required(),
|
|
28
|
+
});
|
|
29
|
+
export const decidePayloadSchema = Joi.object({
|
|
30
|
+
approvalId: id.required(),
|
|
31
|
+
decision: Joi.string().valid('approve', 'decline').required(),
|
|
32
|
+
});
|
|
33
|
+
export const internalDecideSchema = Joi.object({
|
|
34
|
+
userId: id.required(),
|
|
35
|
+
decision: Joi.string().valid('approve', 'decline').required(),
|
|
36
|
+
channel: Joi.string()
|
|
37
|
+
.valid(...APPROVAL_CHANNELS)
|
|
38
|
+
.required(),
|
|
39
|
+
});
|
|
40
|
+
export const redeemSchema = Joi.object({
|
|
41
|
+
token: Joi.string().max(4096).required(),
|
|
42
|
+
userId: id.required(),
|
|
43
|
+
conversationId: id.required(),
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/approvals/schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ApprovalChannel, type ApprovalDecideResult, type ApprovalDecision, type ApprovalRedeemResult, type ApprovalRefusal, type ApprovalStatus, type PendingApprovalInput } from '@ouispec/agent-core';
|
|
2
|
+
import type { RealtimeLogger } from '../logger.js';
|
|
3
|
+
import type { ApprovalTokenSigner } from './token.js';
|
|
4
|
+
/** The Redis command the store uses. ioredis satisfies it. */
|
|
5
|
+
export interface ApprovalRedis {
|
|
6
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface ApprovalStore {
|
|
9
|
+
/** Keep a pending approval until it expires. */
|
|
10
|
+
create(input: PendingApprovalInput): Promise<{
|
|
11
|
+
ok: true;
|
|
12
|
+
approvalId: string;
|
|
13
|
+
argsHash: string;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
} | ApprovalRefusal>;
|
|
16
|
+
/** The user's decision. An approval's token is returned only here, to the decider. */
|
|
17
|
+
decide(approvalId: string, by: {
|
|
18
|
+
userId: string;
|
|
19
|
+
decision: ApprovalDecision;
|
|
20
|
+
channel: ApprovalChannel;
|
|
21
|
+
}): Promise<ApprovalDecideResult>;
|
|
22
|
+
/** Atomic single use: the stored call, for a valid, unexpired, unused token of this user and conversation. */
|
|
23
|
+
redeem(token: string, caller: {
|
|
24
|
+
userId: string;
|
|
25
|
+
conversationId: string;
|
|
26
|
+
}): Promise<ApprovalRedeemResult>;
|
|
27
|
+
/** Where an approval of this user's stands, or null when it is not theirs or not known. */
|
|
28
|
+
status(approvalId: string, userId: string): Promise<ApprovalStatus | null>;
|
|
29
|
+
}
|
|
30
|
+
/** How long a decline is remembered for the turn that follows it. */
|
|
31
|
+
export declare const DECLINED_TTL_SEC: number;
|
|
32
|
+
export declare function createApprovalStore(redis: ApprovalRedis, signer: ApprovalTokenSigner, logger: RealtimeLogger): ApprovalStore;
|
|
33
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/approvals/store.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EAEpB,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1F;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,eAAe,CAAC,CAAC;IACtI,sFAAsF;IACtF,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACxI,8GAA8G;IAC9G,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzG,2FAA2F;IAC3F,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAC5E;AAED,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,QAAU,CAAC;AA4ExC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,cAAc,GAAG,aAAa,CAmK5H"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval store (ADR-0228 §2.4), beside the UI action result store and on
|
|
3
|
+
* the same Redis, so every realtime instance sees every approval:
|
|
4
|
+
*
|
|
5
|
+
* approval:{id} → the pending call as the worker stored it (JSON,
|
|
6
|
+
* never rewritten), TTL until it expires
|
|
7
|
+
* approval:{id}:state → hash: status (pending | approved), userId,
|
|
8
|
+
* conversationId, tool, argsHash, expiresAt, and
|
|
9
|
+
* once approved the token's jti and the channel
|
|
10
|
+
* approval:declined:{id} → what the next turn tells the model, 30 min
|
|
11
|
+
*
|
|
12
|
+
* Each decision and each redemption is one Lua script, so two instances can
|
|
13
|
+
* never both approve one call, or both redeem one token. The stored call is
|
|
14
|
+
* kept as the worker wrote it and never re-encoded inside Redis: Lua's JSON
|
|
15
|
+
* would round numbers and turn `[]` into `{}`, and the redeemed call must be
|
|
16
|
+
* exactly the one the user approved.
|
|
17
|
+
*/
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import { argsHash as hashOf, MAX_APPROVAL_TTL_MS, } from '@ouispec/agent-core';
|
|
20
|
+
/** How long a decline is remembered for the turn that follows it. */
|
|
21
|
+
export const DECLINED_TTL_SEC = 30 * 60;
|
|
22
|
+
const recordKey = (id) => `approval:${id}`;
|
|
23
|
+
const stateKey = (id) => `approval:${id}:state`;
|
|
24
|
+
const declinedKey = (id) => `approval:declined:${id}`;
|
|
25
|
+
// KEYS: record, state, declined. ARGV: record JSON, ttl ms, userId, conversationId, tool, argsHash, expiresAt.
|
|
26
|
+
// A new approval supersedes an old decline under the same id.
|
|
27
|
+
const CREATE = `
|
|
28
|
+
if redis.call('EXISTS', KEYS[1]) == 1 or redis.call('EXISTS', KEYS[2]) == 1 then return 'exists' end
|
|
29
|
+
redis.call('DEL', KEYS[3])
|
|
30
|
+
redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
31
|
+
redis.call('HSET', KEYS[2], 'status', 'pending', 'userId', ARGV[3], 'conversationId', ARGV[4], 'tool', ARGV[5], 'argsHash', ARGV[6], 'expiresAt', ARGV[7])
|
|
32
|
+
redis.call('PEXPIRE', KEYS[2], ARGV[2])
|
|
33
|
+
return 'ok'`;
|
|
34
|
+
// KEYS: state, record, declined. ARGV: userId, decision, now ms, jti, channel, declined ttl s, declined JSON.
|
|
35
|
+
const DECIDE = `
|
|
36
|
+
if redis.call('EXISTS', KEYS[1]) == 0 then return {'unknown'} end
|
|
37
|
+
local s = redis.call('HMGET', KEYS[1], 'status', 'userId', 'expiresAt')
|
|
38
|
+
if s[2] ~= ARGV[1] then return {'forbidden'} end
|
|
39
|
+
if s[1] ~= 'pending' then return {'decided'} end
|
|
40
|
+
if tonumber(ARGV[3]) >= tonumber(s[3]) then return {'expired'} end
|
|
41
|
+
local record = redis.call('GET', KEYS[2])
|
|
42
|
+
if ARGV[2] == 'approve' then
|
|
43
|
+
redis.call('HSET', KEYS[1], 'status', 'approved', 'jti', ARGV[4], 'channel', ARGV[5])
|
|
44
|
+
return {'approved', record}
|
|
45
|
+
end
|
|
46
|
+
redis.call('DEL', KEYS[1], KEYS[2])
|
|
47
|
+
redis.call('SET', KEYS[3], ARGV[7], 'EX', ARGV[6])
|
|
48
|
+
return {'declined', record}`;
|
|
49
|
+
// KEYS: state, record. ARGV: jti, argsHash, tool, userId, conversationId.
|
|
50
|
+
const REDEEM = `
|
|
51
|
+
if redis.call('EXISTS', KEYS[1]) == 0 then return {'used'} end
|
|
52
|
+
local s = redis.call('HMGET', KEYS[1], 'status', 'jti', 'argsHash', 'tool', 'userId', 'conversationId', 'channel')
|
|
53
|
+
if s[1] ~= 'approved' or s[2] ~= ARGV[1] or s[3] ~= ARGV[2] or s[4] ~= ARGV[3] or s[5] ~= ARGV[4] or s[6] ~= ARGV[5] then
|
|
54
|
+
return {'mismatch'}
|
|
55
|
+
end
|
|
56
|
+
local record = redis.call('GET', KEYS[2])
|
|
57
|
+
redis.call('DEL', KEYS[1], KEYS[2])
|
|
58
|
+
return {'ok', record, s[7]}`;
|
|
59
|
+
// KEYS: state, record, declined. ARGV: userId. A live approval first, then a remembered decline.
|
|
60
|
+
const STATUS = `
|
|
61
|
+
if redis.call('EXISTS', KEYS[1]) == 1 then
|
|
62
|
+
local s = redis.call('HMGET', KEYS[1], 'status', 'userId')
|
|
63
|
+
if s[2] ~= ARGV[1] then return {'unknown'} end
|
|
64
|
+
return {s[1], redis.call('GET', KEYS[2])}
|
|
65
|
+
end
|
|
66
|
+
local d = redis.call('GET', KEYS[3])
|
|
67
|
+
if d then return {'declined', d} end
|
|
68
|
+
return {'unknown'}`;
|
|
69
|
+
const refusal = (reason, error) => ({ ok: false, reason, error });
|
|
70
|
+
const REFUSALS = {
|
|
71
|
+
unknown: 'no such approval is waiting: it was never asked for, was already decided and used, or has expired',
|
|
72
|
+
forbidden: "this approval is another user's",
|
|
73
|
+
decided: 'this approval has already been decided',
|
|
74
|
+
expired: 'this approval has expired',
|
|
75
|
+
used: 'this approval has already been used',
|
|
76
|
+
mismatch: 'this token is not for the stored call',
|
|
77
|
+
};
|
|
78
|
+
export function createApprovalStore(redis, signer, logger) {
|
|
79
|
+
/** An approval as this user may see it. */
|
|
80
|
+
async function read(approvalId, userId) {
|
|
81
|
+
const [status, raw] = ((await redis.eval(STATUS, 3, stateKey(approvalId), recordKey(approvalId), declinedKey(approvalId), userId)) ??
|
|
82
|
+
[]);
|
|
83
|
+
if (status === 'declined' && raw)
|
|
84
|
+
return { kind: 'declined', declined: JSON.parse(raw) };
|
|
85
|
+
if ((status === 'pending' || status === 'approved') && raw)
|
|
86
|
+
return { kind: 'open', status, pending: JSON.parse(raw) };
|
|
87
|
+
return { kind: 'unknown' };
|
|
88
|
+
}
|
|
89
|
+
/** What every log record about an approval carries; the arguments only when their declaration says they are not sensitive. */
|
|
90
|
+
const audit = (p, extra = {}) => ({
|
|
91
|
+
approvalId: p.approvalId,
|
|
92
|
+
userId: p.userId,
|
|
93
|
+
conversationId: p.conversationId,
|
|
94
|
+
tool: p.tool,
|
|
95
|
+
effect: p.effect,
|
|
96
|
+
destructive: p.destructive,
|
|
97
|
+
argsHash: p.argsHash,
|
|
98
|
+
...(p.argsSensitive === false ? { args: p.args } : {}),
|
|
99
|
+
...extra,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
async create(input) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
if (input.approvalId !== input.toolCallId)
|
|
105
|
+
return refusal('invalid', 'approvalId must be the tool call id');
|
|
106
|
+
if (!(input.expiresAt > now))
|
|
107
|
+
return refusal('invalid', 'expiresAt must be in the future');
|
|
108
|
+
if (input.expiresAt - now > MAX_APPROVAL_TTL_MS) {
|
|
109
|
+
return refusal('invalid', `an approval lasts at most ${MAX_APPROVAL_TTL_MS / 60_000} minutes`);
|
|
110
|
+
}
|
|
111
|
+
let computed;
|
|
112
|
+
try {
|
|
113
|
+
computed = await hashOf(input.args);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return refusal('invalid', err instanceof Error ? err.message : String(err));
|
|
117
|
+
}
|
|
118
|
+
if (computed !== input.argsHash)
|
|
119
|
+
return refusal('invalid', 'argsHash does not match the arguments');
|
|
120
|
+
const result = await redis.eval(CREATE, 3, recordKey(input.approvalId), stateKey(input.approvalId), declinedKey(input.approvalId), JSON.stringify(input), input.expiresAt - now, input.userId, input.conversationId, input.tool, input.argsHash, input.expiresAt);
|
|
121
|
+
if (result !== 'ok')
|
|
122
|
+
return refusal('decided', `an approval ${input.approvalId} already exists`);
|
|
123
|
+
logger.info(audit(input, { expiresAt: input.expiresAt }), 'Approval stored');
|
|
124
|
+
return { ok: true, approvalId: input.approvalId, argsHash: input.argsHash, expiresAt: input.expiresAt };
|
|
125
|
+
},
|
|
126
|
+
async decide(approvalId, { userId, decision, channel }) {
|
|
127
|
+
const jti = crypto.randomUUID();
|
|
128
|
+
// What a decline is remembered by: the call's tool and title, from its
|
|
129
|
+
// record, which never changes once stored.
|
|
130
|
+
const seen = await read(approvalId, userId);
|
|
131
|
+
const declined = {
|
|
132
|
+
userId,
|
|
133
|
+
tool: seen.kind === 'open' ? seen.pending.tool : '',
|
|
134
|
+
title: seen.kind === 'open' ? seen.pending.preview.title : '',
|
|
135
|
+
};
|
|
136
|
+
const [outcome, raw] = (await redis.eval(DECIDE, 3, stateKey(approvalId), recordKey(approvalId), declinedKey(approvalId), userId, decision, Date.now(), jti, channel, DECLINED_TTL_SEC, JSON.stringify(declined)));
|
|
137
|
+
if (outcome !== 'approved' && outcome !== 'declined') {
|
|
138
|
+
logger.warn({ approvalId, userId, decision, channel, reason: outcome }, 'Approval decision refused');
|
|
139
|
+
return refusal(outcome, REFUSALS[outcome] ?? outcome);
|
|
140
|
+
}
|
|
141
|
+
const pending = JSON.parse(raw);
|
|
142
|
+
if (outcome === 'declined') {
|
|
143
|
+
logger.info(audit(pending, { channel }), 'Approval declined');
|
|
144
|
+
return { ok: true, decision: 'decline', approvalId };
|
|
145
|
+
}
|
|
146
|
+
const token = signer.sign({
|
|
147
|
+
aid: approvalId,
|
|
148
|
+
sub: pending.userId,
|
|
149
|
+
cid: pending.conversationId,
|
|
150
|
+
tool: pending.tool,
|
|
151
|
+
ah: pending.argsHash,
|
|
152
|
+
eff: pending.effect,
|
|
153
|
+
ch: channel,
|
|
154
|
+
iat: Math.floor(Date.now() / 1000),
|
|
155
|
+
exp: Math.floor(pending.expiresAt / 1000),
|
|
156
|
+
jti,
|
|
157
|
+
});
|
|
158
|
+
logger.info(audit(pending, { channel, expiresAt: pending.expiresAt }), 'Approval approved');
|
|
159
|
+
return { ok: true, decision: 'approve', approvalId, token, argsHash: pending.argsHash, expiresAt: pending.expiresAt, channel };
|
|
160
|
+
},
|
|
161
|
+
async redeem(token, caller) {
|
|
162
|
+
const checked = signer.verify(token);
|
|
163
|
+
if (!checked.ok) {
|
|
164
|
+
logger.warn({ userId: caller.userId, conversationId: caller.conversationId, reason: checked.reason, error: checked.error }, 'Approval redemption refused');
|
|
165
|
+
return refusal(checked.reason, checked.reason === 'expired' ? REFUSALS.expired : `not a valid approval token: ${checked.error}`);
|
|
166
|
+
}
|
|
167
|
+
const { claims } = checked;
|
|
168
|
+
const context = { approvalId: claims.aid, userId: caller.userId, tool: claims.tool, effect: claims.eff, channel: claims.ch, argsHash: claims.ah };
|
|
169
|
+
if (claims.sub !== caller.userId || claims.cid !== caller.conversationId) {
|
|
170
|
+
logger.warn({ ...context, tokenUser: claims.sub, reason: 'forbidden' }, 'Approval redemption refused');
|
|
171
|
+
return refusal('forbidden', "this approval is another user's or another conversation's");
|
|
172
|
+
}
|
|
173
|
+
const [outcome, raw, channel] = (await redis.eval(REDEEM, 2, stateKey(claims.aid), recordKey(claims.aid), claims.jti, claims.ah, claims.tool, claims.sub, claims.cid));
|
|
174
|
+
if (outcome !== 'ok') {
|
|
175
|
+
logger.warn({ ...context, reason: outcome }, 'Approval redemption refused');
|
|
176
|
+
return refusal(outcome, REFUSALS[outcome] ?? outcome);
|
|
177
|
+
}
|
|
178
|
+
const p = JSON.parse(raw);
|
|
179
|
+
const call = {
|
|
180
|
+
approvalId: p.approvalId,
|
|
181
|
+
toolCallId: p.toolCallId,
|
|
182
|
+
conversationId: p.conversationId,
|
|
183
|
+
turnId: p.turnId,
|
|
184
|
+
userId: p.userId,
|
|
185
|
+
tool: p.tool,
|
|
186
|
+
args: p.args,
|
|
187
|
+
argsHash: p.argsHash,
|
|
188
|
+
effect: p.effect,
|
|
189
|
+
destructive: p.destructive,
|
|
190
|
+
channel: channel,
|
|
191
|
+
};
|
|
192
|
+
logger.info(audit(p, { channel }), 'Approval redeemed');
|
|
193
|
+
return { ok: true, call };
|
|
194
|
+
},
|
|
195
|
+
async status(approvalId, userId) {
|
|
196
|
+
const seen = await read(approvalId, userId);
|
|
197
|
+
if (seen.kind === 'declined') {
|
|
198
|
+
return seen.declined.userId === userId ? { approvalId, status: 'declined', tool: seen.declined.tool, title: seen.declined.title } : null;
|
|
199
|
+
}
|
|
200
|
+
if (seen.kind === 'open')
|
|
201
|
+
return { approvalId, status: seen.status, tool: seen.pending.tool, title: seen.pending.preview.title };
|
|
202
|
+
return null;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/approvals/store.ts"],"names":[],"mappings":"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"}
|