@mercury-fw/channel-http 0.1.0 → 0.2.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 +19 -0
- package/README.md +4 -4
- package/dist/http-server.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/http-server.ts +17 -2
- package/index.ts +4 -2
- package/openapi.yaml +7 -4
- package/package.json +5 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @mercury-fw/channel-http
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8bed46b: - A turn carries a `Principal` (the person behind it and the provider that vouched for them) instead of the opaque `userId` and `wikiUserId`; the core derives every per-person id from it.
|
|
8
|
+
- Episodic memory follows the principal, not the channel: a session is captured whenever a provider vouched for the person. Google Chat keeps the ids it had; the terminal and the HTTP channel (until it authenticates its callers) send a principal nobody vouched for, so they stay out of episodic memory as before.
|
|
9
|
+
- The HTTP channel accepts a `conversationId` on `/turn` and `/confirm` only made of letters, digits, `-` and `_`, up to 128 characters once trimmed, and answers `400` otherwise: a client-chosen id can no longer reach another user's wiki notes, forge log lines or break the turn. An existing HTTP conversation whose id has other characters can still be read with `/conversation`, but not continued. `/conversation` keeps opening any session key `/conversations` lists, whatever channel it came from.
|
|
10
|
+
- The model's wiki read tools refuse a per-user id that isn't one path segment, as writes already did.
|
|
11
|
+
- `CHANNEL_API_VERSION` is 2: a channel written for version 1 is refused at load.
|
|
12
|
+
- The first-party channels declare their channel api version as a literal, so an older channel can't claim a newer contract it doesn't implement.
|
|
13
|
+
|
|
14
|
+
## 0.1.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 7926aa2: - Wiki grep ignores case, for the agent, the nightly review, `mfw vault grep` and the HTTP `/wiki/grep` route.
|
|
19
|
+
- The agent's `write_file` and the nightly review's `write_curated` take a path starting with `curated/`, as listing, reading and grepping give it, instead of writing under `curated/curated/`.
|
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
|
|
|
36
36
|
| field | type | required | description |
|
|
37
37
|
|---|---|---|---|
|
|
38
38
|
| `text` | string | yes | The user message. A bare confirmation token here confirms a staged action (see the `pending` event) without invoking the model. |
|
|
39
|
-
| `conversationId` | string | no | Opaque, client-owned id that continues a conversation. Omitted ⇒ a fresh one-off session. |
|
|
39
|
+
| `conversationId` | string | no | Opaque, client-owned id that continues a conversation: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Omitted ⇒ a fresh one-off session. |
|
|
40
40
|
|
|
41
|
-
**Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing
|
|
41
|
+
**Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing, the body isn't JSON, or `conversationId` has other characters than the ones above.
|
|
42
42
|
|
|
43
43
|
Reasoning and answer text arrive as **incremental deltas** — never one finished block — so a UI renders them live; treat the `final` event as the authoritative text. **To cancel** a turn (e.g. a generation that's diverging), close the connection: Mercury aborts the in-flight generation.
|
|
44
44
|
|
|
@@ -63,7 +63,7 @@ curl -N -X POST http://localhost:4100/turn \
|
|
|
63
63
|
|
|
64
64
|
## `POST /confirm`
|
|
65
65
|
|
|
66
|
-
Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. `400` if `token` or `conversationId` is missing. Never invokes the model.
|
|
66
|
+
Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. `400` if `token` or `conversationId` is missing, or the id has other characters than `/turn` accepts. Never invokes the model.
|
|
67
67
|
|
|
68
68
|
## Read-only introspection
|
|
69
69
|
|
|
@@ -71,7 +71,7 @@ All `GET`, all JSON, all reporting state already held in-process.
|
|
|
71
71
|
|
|
72
72
|
| endpoint | `data` on success |
|
|
73
73
|
|---|---|
|
|
74
|
-
| `GET /conversation?id=<conversationId>&limit=<n>&offset=<cursor>` | `{ messages: [{ role, content, timestamp }], nextOffset }` — a conversation's durable transcript in order; `400` if `id` is missing |
|
|
74
|
+
| `GET /conversation?id=<conversationId>&limit=<n>&offset=<cursor>` | `{ messages: [{ role, content, timestamp }], nextOffset }` — a conversation's durable transcript in order; `400` if `id` is missing. Any session key `/conversations` lists opens here, whatever channel it came from |
|
|
75
75
|
| `GET /conversations?limit=<n>` | `{ conversations: [{ sessionKey, lastTimestamp, preview }] }` — known conversations, most-recently-active first |
|
|
76
76
|
| `GET /manifest` | `{ manifest: { coreApiVersion, plugins: [{ name, apiVersion, active, skills, hasBuild, customStatus }], activeClis, skills } }` |
|
|
77
77
|
| `GET /confirmations` | `{ pending: [{ sessionKey, binary, args, expiresAt }] }` — tokens are deliberately never included |
|
package/dist/http-server.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type TurnRequestDeps = {
|
|
|
33
33
|
};
|
|
34
34
|
/**
|
|
35
35
|
* Serves the OpenAPI document that describes this surface — the single source
|
|
36
|
-
* of truth in `
|
|
36
|
+
* of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
|
|
37
37
|
* API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
|
|
38
38
|
* tooling (Redoc, Swagger UI, Postman) reads YAML directly.
|
|
39
39
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
* inversion). Those two are optional on the contract but required here, so a
|
|
12
12
|
* context missing them throws — the loader isolates it fail-soft.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { ChannelPlugin } from "@mercury-fw/channel-types";
|
|
15
15
|
export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
|
|
16
16
|
export declare const httpChannel: ChannelPlugin;
|
package/http-server.ts
CHANGED
|
@@ -26,6 +26,13 @@ import {
|
|
|
26
26
|
type ConfirmOutcome,
|
|
27
27
|
} from "@mercury-fw/channel-types";
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* A client's conversation id becomes the session key, a log prefix and (until
|
|
31
|
+
* the channel authenticates) the per-person wiki id, so only letters, digits,
|
|
32
|
+
* `-` and `_` get in: no path segments, no line breaks, nothing to encode.
|
|
33
|
+
*/
|
|
34
|
+
const CONVERSATION_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
35
|
+
|
|
29
36
|
/** Resolves a bare confirmation token to a reply string, or `null` if the input isn't a token. Injected by the core (`ctx.confirm`). */
|
|
30
37
|
export type ConfirmFn = (token: string, sessionKey: string, userId: string) => Promise<string | null>;
|
|
31
38
|
/** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
|
|
@@ -68,7 +75,7 @@ function preflight(origin: string): Response {
|
|
|
68
75
|
|
|
69
76
|
/**
|
|
70
77
|
* Serves the OpenAPI document that describes this surface — the single source
|
|
71
|
-
* of truth in `
|
|
78
|
+
* of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
|
|
72
79
|
* API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
|
|
73
80
|
* tooling (Redoc, Swagger UI, Postman) reads YAML directly.
|
|
74
81
|
*/
|
|
@@ -105,6 +112,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
105
112
|
typeof body.conversationId === "string" && body.conversationId.trim().length > 0
|
|
106
113
|
? body.conversationId.trim()
|
|
107
114
|
: (deps.newSessionKey ?? (() => crypto.randomUUID()))();
|
|
115
|
+
if (!CONVERSATION_ID.test(sessionKey)) {
|
|
116
|
+
return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
|
|
117
|
+
}
|
|
108
118
|
|
|
109
119
|
// Aborted when the client disconnects (see the stream's `cancel` below) so
|
|
110
120
|
// the in-flight turn stops instead of running to completion — the "stop"
|
|
@@ -164,7 +174,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
164
174
|
multiUser: false,
|
|
165
175
|
text,
|
|
166
176
|
sessionKey,
|
|
167
|
-
|
|
177
|
+
// Unauthenticated: the conversation id stands in for the caller,
|
|
178
|
+
// and nobody vouches for it.
|
|
179
|
+
principal: { id: sessionKey, provider: "none" },
|
|
168
180
|
logPrefix: `[http:${sessionKey}] `,
|
|
169
181
|
abortSignal: abort.signal,
|
|
170
182
|
},
|
|
@@ -218,6 +230,9 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
|
|
|
218
230
|
if (token.length === 0 || sessionKey.length === 0) {
|
|
219
231
|
return Response.json({ ok: false, error: "missing token or conversationId" }, { status: 400, headers: cors });
|
|
220
232
|
}
|
|
233
|
+
if (!CONVERSATION_ID.test(sessionKey)) {
|
|
234
|
+
return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
|
|
235
|
+
}
|
|
221
236
|
const outcome = await deps.resolveConfirmation(token, sessionKey, sessionKey);
|
|
222
237
|
switch (outcome.status) {
|
|
223
238
|
case "not-a-token":
|
package/index.ts
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
* inversion). Those two are optional on the contract but required here, so a
|
|
12
12
|
* context missing them throws — the loader isolates it fail-soft.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { ChannelPlugin } from "@mercury-fw/channel-types";
|
|
15
15
|
import { createHttpProvider } from "./http-provider.ts";
|
|
16
16
|
|
|
17
17
|
export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
|
|
18
18
|
|
|
19
19
|
export const httpChannel: ChannelPlugin = {
|
|
20
|
-
|
|
20
|
+
// The contract this channel is written for, as a literal: importing
|
|
21
|
+
// CHANNEL_API_VERSION would report whichever contract is installed.
|
|
22
|
+
apiVersion: 2,
|
|
21
23
|
name: "http",
|
|
22
24
|
build: (ctx) => {
|
|
23
25
|
if (!ctx.resolveConfirmation || !ctx.reads) {
|
package/openapi.yaml
CHANGED
|
@@ -69,7 +69,7 @@ paths:
|
|
|
69
69
|
schema:
|
|
70
70
|
$ref: "#/components/schemas/TurnEvent"
|
|
71
71
|
"400":
|
|
72
|
-
description: Missing/invalid body.
|
|
72
|
+
description: Missing/invalid body, or a conversationId with characters outside the allowed ones.
|
|
73
73
|
content:
|
|
74
74
|
application/json:
|
|
75
75
|
schema:
|
|
@@ -98,7 +98,7 @@ paths:
|
|
|
98
98
|
schema:
|
|
99
99
|
$ref: "#/components/schemas/ConfirmResult"
|
|
100
100
|
"400":
|
|
101
|
-
description: Missing token or conversationId.
|
|
101
|
+
description: Missing token or conversationId, or a conversationId with characters outside the allowed ones.
|
|
102
102
|
content:
|
|
103
103
|
application/json:
|
|
104
104
|
schema:
|
|
@@ -110,7 +110,7 @@ paths:
|
|
|
110
110
|
- name: id
|
|
111
111
|
in: query
|
|
112
112
|
required: true
|
|
113
|
-
description: The
|
|
113
|
+
description: The session key whose messages to return, as `/conversations` lists it (any channel's).
|
|
114
114
|
schema:
|
|
115
115
|
type: string
|
|
116
116
|
- name: limit
|
|
@@ -234,6 +234,7 @@ paths:
|
|
|
234
234
|
- name: pattern
|
|
235
235
|
in: query
|
|
236
236
|
required: true
|
|
237
|
+
description: A regular expression, matched line by line, case ignored.
|
|
237
238
|
schema:
|
|
238
239
|
type: string
|
|
239
240
|
responses:
|
|
@@ -303,7 +304,8 @@ components:
|
|
|
303
304
|
description: The user's message, or a bare confirmation token.
|
|
304
305
|
conversationId:
|
|
305
306
|
type: string
|
|
306
|
-
|
|
307
|
+
pattern: "^[A-Za-z0-9_-]{1,128}$"
|
|
308
|
+
description: Opaque, client-owned; letters, digits, `-` and `_` (a UUID fits). Omit to start a fresh ephemeral conversation.
|
|
307
309
|
TurnEvent:
|
|
308
310
|
type: object
|
|
309
311
|
description: The shape carried by each SSE `data:` line, per event type.
|
|
@@ -349,6 +351,7 @@ components:
|
|
|
349
351
|
type: string
|
|
350
352
|
conversationId:
|
|
351
353
|
type: string
|
|
354
|
+
pattern: "^[A-Za-z0-9_-]{1,128}$"
|
|
352
355
|
ConfirmResult:
|
|
353
356
|
type: object
|
|
354
357
|
required: [ok, resolved]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@mercury-fw/channel-types": ">=0.
|
|
21
|
+
"@mercury-fw/channel-types": ">=0.34.0 <1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@mercury-fw/channel-types": "0.
|
|
25
|
-
"@mercury-fw/plugin-types": "0.
|
|
24
|
+
"@mercury-fw/channel-types": "0.34.0",
|
|
25
|
+
"@mercury-fw/plugin-types": "0.34.0",
|
|
26
26
|
"@mercury-fw/typescript-config": "*",
|
|
27
|
-
"@types/bun": "^1.4.
|
|
27
|
+
"@types/bun": "^1.4.2",
|
|
28
28
|
"typescript": "^6.0.3"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|