@egox/contracts 0.5.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/src/ask.d.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @egox/contracts — `/ask` request + response wire types.
3
+ *
4
+ * SSE event types for `/ask/stream` live in `stream.d.ts` — they share
5
+ * the request body via `AskStreamRequestBody` declared here.
6
+ */
7
+
8
+ import type { Intent, JsonSchemaDefinition, ResponseFormat, TokenUsage } from './envelope';
9
+
10
+ // ============================================================================
11
+ // /ask — request
12
+ // ============================================================================
13
+
14
+ /**
15
+ * **Wire** body of `POST /egox/ask` and `POST /egox/ask/stream`.
16
+ *
17
+ * This is exactly what gets serialized into the request body. Consumers
18
+ * (SDK, MCP previews, raw `curl` users) and the backend route handler MUST
19
+ * agree on this shape.
20
+ *
21
+ * Things that look like they should live here but don't:
22
+ * - `tenantId` is sent via `X-Tenant-Id` header (or derived from API key).
23
+ * - `authToken` is sent via `Authorization: Bearer …` header.
24
+ * - `incomingHeaders` is captured from the actual HTTP headers, not the body.
25
+ * These are intentionally NOT body fields — they live where HTTP puts them.
26
+ */
27
+ export interface AskRequestBody {
28
+ /** End-user's message for this turn. Required, non-empty. */
29
+ message: string;
30
+
31
+ /**
32
+ * Continue an existing conversation by passing back the EgoX-internal
33
+ * UUID returned in a previous response. The two failure modes the
34
+ * legacy single-`threadId` model silently collapsed are now loud:
35
+ * - A well-formed UUID with no matching row → 404 NotFound.
36
+ * - A non-UUID string → 400 InvalidInput.
37
+ * If you want to use your own conversation id (e.g. `conv:abc123`),
38
+ * send it as `externalThreadId` instead and EgoX will mint the
39
+ * UUID for you on first call and re-use it on every subsequent call
40
+ * with the same external id.
41
+ *
42
+ * Mutually exclusive with `externalThreadId` in practice — when both
43
+ * are supplied `threadId` wins for back-compat (the backend logs a
44
+ * debug warning).
45
+ */
46
+ threadId?: string;
47
+
48
+ /**
49
+ * **Opaque** consumer-supplied conversation identifier (Phase 6).
50
+ * Sent on every call in the same conversation so EgoX can resolve
51
+ * to the same thread row without the caller ever round-tripping
52
+ * the EgoX UUID. Shape is yours to choose (`conv:abc123`,
53
+ * `thread:user42:session99`, hashed ids, …); EgoX persists it
54
+ * verbatim and never parses it.
55
+ *
56
+ * Resolution semantics:
57
+ * - First call with this id → EgoX creates a new thread with the
58
+ * id permanently attached. The minted EgoX UUID is also
59
+ * returned on `AskResponseData.threadId` for debugging.
60
+ * - Subsequent calls with the same id → same thread row, no
61
+ * matter what UUID was previously returned.
62
+ *
63
+ * Echoed back unchanged on `AskResponseData.externalThreadId` so
64
+ * the caller can confirm which conversation their request landed in.
65
+ */
66
+ externalThreadId?: string;
67
+
68
+ /**
69
+ * **Opaque** end-user identifier from the consumer's POV — used by EgoX
70
+ * for thread association, audit, and pass-through to tools. Any string
71
+ * shape is accepted (UUID, slug, hash, …); EgoX never resolves it
72
+ * against its own admin tables.
73
+ */
74
+ externalUserId?: string;
75
+
76
+ /** Override the tenant's default response format for this single call. */
77
+ responseFormat?: ResponseFormat;
78
+
79
+ /**
80
+ * Required when `responseFormat === 'json_object'`. Injected into the
81
+ * system prompt so the LLM produces conforming JSON.
82
+ */
83
+ jsonSchema?: JsonSchemaDefinition;
84
+
85
+ /**
86
+ * Free-form per-call context forwarded to tool calls and echoed back
87
+ * on the response. Use for tenant/user context your tools need.
88
+ */
89
+ metadata?: Record<string, unknown>;
90
+
91
+ /**
92
+ * Per-call override of the tenant's `persist_message_content` default.
93
+ *
94
+ * - `true` → metadata-only for THIS call: EgoX does not persist
95
+ * the user message or the answer text. The thread/
96
+ * message rows still exist (so usage, intent, model,
97
+ * tokens, and the thread correlation survive for
98
+ * analytics) but `content` is `NULL` and the row is
99
+ * flagged `contentRedacted: true`.
100
+ * - `false` → persist text for THIS call **only if the tenant
101
+ * default already allows it**. The tenant's
102
+ * `persist_message_content=false` is a hard floor:
103
+ * `noStore:false` CANNOT re-enable storage on a
104
+ * no-store tenant. It only matters on a tenant that
105
+ * persists by default (where it's a no-op).
106
+ * - omitted → use the tenant's `persist_message_content` setting.
107
+ *
108
+ * Net: a per-call flag can only ever *tighten* (force no-store), never
109
+ * loosen a tenant that has opted out of storing conversations.
110
+ *
111
+ * Built for integrations whose policy is "don't store conversations"
112
+ * (e.g. .collab): set the tenant default to no-store, or pass this
113
+ * per call. When no-store is in effect EgoX keeps no transcript, so
114
+ * multi-turn context must be supplied by the caller (e.g. via a
115
+ * `getConversation` tool) rather than reloaded from EgoX history.
116
+ */
117
+ noStore?: boolean;
118
+
119
+ /**
120
+ * Per-call header map forwarded to the tool the LLM ends up calling,
121
+ * gated by that tool's `forwardHeaders` allowlist. Use this when the
122
+ * value:
123
+ * - is per-end-user (so it can't be baked into the tool's static
124
+ * `headers` config), AND
125
+ * - shares a name with a header EgoX claims for itself (most
126
+ * commonly `Authorization` and `X-API-Key` — both used to
127
+ * authenticate the caller TO EgoX, so they're not free for
128
+ * forwarding via the inbound-HTTP path).
129
+ *
130
+ * Body-sourced entries skip the inbound-HTTP forward blocklist
131
+ * (you opted in by putting them here), but EgoX-internal namespaces
132
+ * (`x-egox-*`, `x-mcp-*`) are still rejected.
133
+ *
134
+ * Header names are case-insensitive on the wire and normalized to
135
+ * the canonical casing on the tool's `forwardHeaders` allowlist
136
+ * before going out on the outbound tool request. Values must be
137
+ * strings; binary headers are not supported.
138
+ *
139
+ * Source precedence on the outbound tool request (later wins):
140
+ * 1. inbound-HTTP forwarded headers (legacy path, blocklist applied)
141
+ * 2. body `toolHeaders` (this field — body wins on name conflicts)
142
+ * 3. tool's static `headers` config (tool-owner default)
143
+ * 4. `Authorization` injected by `authType: 'FORWARD'`
144
+ */
145
+ toolHeaders?: Record<string, string>;
146
+ }
147
+
148
+ /**
149
+ * **Wire** body of `POST /egox/ask/stream`. Identical to `AskRequestBody`
150
+ * plus an opt-in stream-detail flag.
151
+ *
152
+ * `stream`:
153
+ * - `true` (default when omitted) → full thinking timeline (intent, rag.*,
154
+ * tool.*, delta, done).
155
+ * - `'minimal'` → only intent, delta, done, error.
156
+ */
157
+ export interface AskStreamRequestBody extends AskRequestBody {
158
+ stream?: true | 'minimal';
159
+
160
+ /**
161
+ * Opt-in deep-execution trace for the Console "Your Agent" playground.
162
+ *
163
+ * SECURITY: ignored unless the request is authenticated with a console
164
+ * JWT. API-key and MCP callers cannot enable debug — the route handler
165
+ * silently coerces the flag to `false` for those auth paths.
166
+ *
167
+ * When enabled, the stream additionally emits:
168
+ * - `llm.debug` after each LLM iteration (messages array, tool-call
169
+ * choice, finish reason, model, usage)
170
+ * - `tool.debug` after each tool execution (request method/url/
171
+ * headers/body, response status/headers/body, attempts)
172
+ *
173
+ * Sensitive header values (Authorization, X-Egox-Signature,
174
+ * X-Egox-Timestamp, and anything matching /token|secret|key|auth/i)
175
+ * are redacted to `***` before transmission.
176
+ *
177
+ * Implies `stream: true` (full detail) — `'minimal'` + `debug` is a
178
+ * 400 InvalidInput.
179
+ */
180
+ debug?: boolean;
181
+ }
182
+
183
+ // ============================================================================
184
+ // /ask — response
185
+ // ============================================================================
186
+
187
+ /**
188
+ * **Wire** payload of `POST /egox/ask`, carried inside `EgoxApiResponse.data`.
189
+ *
190
+ * IMPORTANT — the field name is `answer`, not `response`. Both the backend
191
+ * orchestrator's return type and the SDK's decoder reference this interface,
192
+ * which is the structural guard against the two of them ever drifting again.
193
+ */
194
+ export interface AskResponseData {
195
+ /** Thread the message was appended to (new or existing). */
196
+ threadId: string;
197
+ /**
198
+ * Echo of `AskRequestBody.externalThreadId` when one was supplied —
199
+ * either the consumer-supplied id used to resolve an existing thread,
200
+ * or the id that was just attached to a freshly-created one. Absent
201
+ * when the caller didn't supply an `externalThreadId` (Phase 6).
202
+ */
203
+ externalThreadId?: string;
204
+ /** The LLM's reply text. Empty string when the model produced no text. */
205
+ answer: string;
206
+ intent: Intent;
207
+ /** Name of the tool the LLM invoked, or `null` if no tool ran. */
208
+ toolUsed: string | null;
209
+ /** How many RAG chunks were injected into context. `0` when RAG was off / unused. */
210
+ ragChunksUsed: number;
211
+ usage: TokenUsage;
212
+ /** Resolved model id, e.g. `gpt-4.1-mini-2025-04-14`. */
213
+ model: string;
214
+ /** Echo of `AskRequestBody.metadata`, unchanged. */
215
+ metadata?: Record<string, unknown>;
216
+ /**
217
+ * Pending high-impact actions awaiting user approval (Path-A, DotCollab #8).
218
+ * Populated (non-empty) when a gated tool returned an `approval_requested`
219
+ * envelope this turn instead of executing — the consumer renders an
220
+ * Approve/Reject card per entry and resolves it out-of-band (e.g. a
221
+ * `.../approvals/:approvalId/resolve` route). Absent/empty otherwise, so
222
+ * consumers that ignore it are unaffected (Path-B native-approvals path).
223
+ */
224
+ pendingActions?: Array<{ approvalId: string; toolName: string; summary: string }>;
225
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @egox/contracts — common envelope + shared primitives.
3
+ *
4
+ * Cross-family types that every other contract file ends up importing.
5
+ * Lives in its own file so a future family (RAG, tenants, …) can pull
6
+ * just what it needs without dragging the whole `/ask` surface along.
7
+ *
8
+ * Versioning policy (also in package README, kept here for any reader
9
+ * who lands on this file first):
10
+ * - Additive optional field → minor bump, non-breaking.
11
+ * - Rename / remove → major bump, coordinated release of
12
+ * backend + SDK + MCP + console.
13
+ */
14
+
15
+ /**
16
+ * Standard envelope every JSON endpoint wraps its response in.
17
+ * - `status: 'OK'` → success, `data` is present and well-formed.
18
+ * - `status: 'KO'` → failure, `code` + `message` describe the problem.
19
+ */
20
+ export interface EgoxApiResponse<T> {
21
+ status: 'OK' | 'KO';
22
+ data?: T;
23
+ message?: string;
24
+ code?: string;
25
+ }
26
+
27
+ /**
28
+ * What the orchestrator decided this turn was about. Drives prompt /
29
+ * tool selection in the backend; surfaced verbatim to consumers so they
30
+ * can attribute behaviour ("which turns hit RAG?", "which used tools?").
31
+ */
32
+ export type Intent = 'vanilla' | 'rag' | 'tools' | 'rag_tools';
33
+
34
+ /**
35
+ * Output format the LLM was asked to produce.
36
+ * - `'text'` → free-form natural language (default).
37
+ * - `'json_object'` → strict JSON, requires `jsonSchema` on the request.
38
+ */
39
+ export type ResponseFormat = 'text' | 'json_object';
40
+
41
+ /**
42
+ * JSON Schema fragment used for structured-output (`/ask`'s
43
+ * `responseFormat: 'json_object'`). Intentionally narrow: the wire only
44
+ * accepts an object root with named properties — that's what the LLM
45
+ * constrained-decoders expect.
46
+ *
47
+ * NOT the same as the recursive `JsonSchema` shape used by tool
48
+ * input/output definitions (see `tool.d.ts`). Two different surfaces,
49
+ * two different shapes — don't conflate.
50
+ */
51
+ export interface JsonSchemaDefinition {
52
+ type: 'object';
53
+ properties: Record<string, unknown>;
54
+ required?: string[];
55
+ description?: string;
56
+ }
57
+
58
+ /**
59
+ * Token accounting echoed back on every `/ask` and the `done` SSE frame.
60
+ * Useful for per-call cost attribution on the consumer side.
61
+ */
62
+ export interface TokenUsage {
63
+ promptTokens: number;
64
+ completionTokens: number;
65
+ totalTokens: number;
66
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @egox/contracts
3
+ *
4
+ * Wire-format type contracts shared between every EgoX surface that talks
5
+ * over HTTP / SSE — the backend (`backend/egox`), the Node SDK
6
+ * (`@egox/client`), the MCP server (`egox-mcp`), and the console.
7
+ *
8
+ * **Types only.** This file (and every file it re-exports) deliberately
9
+ * exports nothing at runtime so consumers can
10
+ * `import type { ... } from '@egox/contracts'` and pay zero bundle cost,
11
+ * and so a missing path-alias in one tsconfig can never cause a runtime
12
+ * ReferenceError. Per-package runtime constants (e.g.
13
+ * `SDK_RESERVED_HEADERS` in the SDK, `HEADER_FORWARD_BLOCKLIST` on the
14
+ * backend) live next to the code that needs them.
15
+ *
16
+ * **Single source of truth.** Field names and shapes here are
17
+ * authoritative for the wire. Backend response builders, SDK decoders,
18
+ * and MCP previews MUST conform to these types — that is what prevents
19
+ * the kind of silent `answer === undefined` bug we hit when the SDK was
20
+ * reading `.response` while the backend was writing `.answer`.
21
+ *
22
+ * **Layout.** One file per endpoint family (Phase 9):
23
+ * - `envelope.d.ts` — `EgoxApiResponse`, `Intent`, `ResponseFormat`,
24
+ * `JsonSchemaDefinition`, `TokenUsage`.
25
+ * - `ask.d.ts` — `AskRequestBody`, `AskStreamRequestBody`,
26
+ * `AskResponseData`.
27
+ * - `stream.d.ts` — full `AskStreamEvent` discriminated union +
28
+ * every variant.
29
+ * - `tool.d.ts` — `ToolWire`, `ToolCreateBody`, `ToolUpdateBody`,
30
+ * `ToolListResponseData`, `ToolGetResponseData`,
31
+ * `ToolPreview`, vocabularies (`ToolType`,
32
+ * `ToolHttpMethod`, `ToolAuthType`, `ToolSource`),
33
+ * recursive `JsonSchemaWire`.
34
+ * - `rag.d.ts` — `RagDocumentWire`, `RagDocumentCreateBody`,
35
+ * `RagDocumentUpsertByTitleBody`,
36
+ * `RagDocumentListResponseData`,
37
+ * `RagDocumentGetResponseData`,
38
+ * `RagDocumentUpsertResponseData`, vocabularies
39
+ * (`RagDocumentSourceType`, `RagProcessingStatus`,
40
+ * `RagDocumentSource`).
41
+ * - `api-key.d.ts` — `ApiKeyWire`, `ApiKeyCreatedWire`,
42
+ * create / rotate bodies, list / get / created
43
+ * response data, vocabulary (`ApiKeyScope`).
44
+ * - `mcp-token.d.ts` — `McpTokenWire`, `McpTokenCreatedWire`, create
45
+ * body, list / get / created response data,
46
+ * vocabulary (`McpTokenScope`).
47
+ * - `tenant.d.ts` — `TenantWire` (snake_case on the wire — see
48
+ * the file header), create / update bodies,
49
+ * list / get / created response data,
50
+ * vocabularies (`ComplianceTier`,
51
+ * `DataResidencyRegion`, `TenantStatus`,
52
+ * `TenantIndustry`).
53
+ * - `thread.d.ts` — `ThreadWire`, `MessageWire`,
54
+ * `ThreadWithStatsWire`, `ThreadWithMessagesWire`,
55
+ * `ToolCallWire`, list / get response data,
56
+ * vocabularies (`MessageRole`,
57
+ * `MessageFailureReason`).
58
+ * - `personality.d.ts` — Both surfaces share this file. User-level:
59
+ * `PersonalityProfileWire`, `PresetDefinitionWire`,
60
+ * create / update bodies, `ActivePersonalityWire`.
61
+ * Project-scoped: `TenantPersonalityProfileWire`,
62
+ * create / update / clone bodies,
63
+ * `ActiveTenantPersonalityWire`. Shared:
64
+ * `PersonalityTraitsWire`, `PresetType`.
65
+ *
66
+ * This barrel re-exports everything so existing consumers' import paths
67
+ * (`import type { AskResponseData } from '@egox/contracts'`) keep
68
+ * working unchanged. Adding a new family is a single-file change here
69
+ * plus a new `export *` line below.
70
+ *
71
+ * **Versioning.**
72
+ * - Additive optional field → minor bump, non-breaking.
73
+ * - Rename / remove → major bump, coordinated release of
74
+ * backend + SDK + MCP + console.
75
+ */
76
+
77
+ export * from './envelope';
78
+ export * from './ask';
79
+ export * from './stream';
80
+ export * from './tool';
81
+ export * from './rag';
82
+ export * from './api-key';
83
+ export * from './mcp-token';
84
+ export * from './tenant';
85
+ export * from './thread';
86
+ export * from './personality';
87
+ export * from './search';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @egox/contracts — `/egox/tenants/:tenantId/mcp-tokens/*` wire types.
3
+ *
4
+ * Per-tenant developer tokens (the `egox_mcp_*` credentials Cursor
5
+ * presents to the EgoX MCP server). Each token is short-lived,
6
+ * tenant-scoped, and revocable.
7
+ *
8
+ * Structurally similar to `api-key.d.ts` but a distinct family:
9
+ * - MCP tokens have a smaller scope vocabulary (no `'ask'`, no
10
+ * `'threads:read'`, no `'admin'`).
11
+ * - MCP tokens carry a `revokedAt` audit field that API keys don't.
12
+ * - MCP tokens have `createdByUserId` provenance (which console
13
+ * user issued it).
14
+ */
15
+
16
+ // ============================================================================
17
+ // Locked vocabularies
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Scopes an MCP token can hold. Mirrors `MCP_SCOPES` in
22
+ * `backend/egox/modules/mcp/token/constants.ts`. The MCP server
23
+ * enforces these in its tool router; the backend re-enforces on
24
+ * every admin route call.
25
+ */
26
+ export type McpTokenScope =
27
+ | 'tools:read'
28
+ | 'tools:write'
29
+ | 'rag:read'
30
+ | 'rag:write';
31
+
32
+ // ============================================================================
33
+ // McpTokenWire — the canonical row shape (no secrets)
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Public-facing MCP token info. Returned by every
38
+ * `GET /egox/tenants/:tenantId/mcp-tokens` and
39
+ * `GET /egox/tenants/:tenantId/mcp-tokens/:tokenId` response.
40
+ *
41
+ * Carries NO secret material — only the display prefix
42
+ * (`egox_mcp_abc12345…`). The raw token is exposed exactly once
43
+ * on create / rotate via `McpTokenCreatedWire`; there is no endpoint
44
+ * that returns it again.
45
+ *
46
+ * Timestamps are ISO 8601 strings (backend's `formatTokenInfo` emits
47
+ * ISO to match the wire — verified by re-anchor in Phase 9.4).
48
+ */
49
+ export interface McpTokenWire {
50
+ id: string;
51
+ tenantId: string;
52
+ name: string;
53
+ /** Public-display prefix; safe to log. */
54
+ tokenPrefix: string;
55
+ scopes: McpTokenScope[];
56
+ expiresAt: string | null;
57
+ /**
58
+ * When the token was revoked. `null` while active. Distinct from
59
+ * `expiresAt`: revocation is a deliberate user action and
60
+ * recorded immediately; expiration is server-clock natural decay.
61
+ */
62
+ revokedAt: string | null;
63
+ /** Server-computed: `true` when `expiresAt` is in the past. */
64
+ isExpired: boolean;
65
+ /**
66
+ * Server-computed convenience flag — `true` only when
67
+ * `revokedAt === null && !isExpired`. Console badge logic should
68
+ * branch on this rather than re-deriving from the raw fields.
69
+ */
70
+ isActive: boolean;
71
+ lastUsedAt: string | null;
72
+ /**
73
+ * Total successful authentications using this token. Denormalised
74
+ * counter updated by the MCP auth middleware on every accepted
75
+ * SSE connection.
76
+ */
77
+ usageCount: number;
78
+ /**
79
+ * Console user who issued the token. `null` for tokens minted
80
+ * via system / migration paths (none today; future-proof).
81
+ */
82
+ createdByUserId: string | null;
83
+ createdAt: string;
84
+ }
85
+
86
+ /**
87
+ * Returned exactly once at creation. The raw `token` is unrecoverable
88
+ * afterwards — only the SHA-256 hash and prefix are persisted
89
+ * server-side. Same one-shot contract as `ApiKeyCreatedWire`.
90
+ */
91
+ export interface McpTokenCreatedWire {
92
+ /** Raw token — show once, then forget. */
93
+ token: string;
94
+ id: string;
95
+ tenantId: string;
96
+ name: string;
97
+ tokenPrefix: string;
98
+ scopes: McpTokenScope[];
99
+ expiresAt: string | null;
100
+ createdAt: string;
101
+ }
102
+
103
+ // ============================================================================
104
+ // Request bodies (what the client SENDS)
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Wire body of `POST /egox/tenants/:tenantId/mcp-tokens`. `tenantId`
109
+ * is the URL param; `createdByUserId` is derived from the console
110
+ * JWT on the request side.
111
+ */
112
+ export interface McpTokenCreateBody {
113
+ name: string;
114
+ scopes?: McpTokenScope[];
115
+ /** Optional TTL in days. Server caps at `MAX_EXPIRY_DAYS`. */
116
+ expiresInDays?: number;
117
+ }
118
+
119
+ // ============================================================================
120
+ // Response data shapes (carried inside `EgoxApiResponse.data`)
121
+ // ============================================================================
122
+
123
+ export interface McpTokenListResponseData {
124
+ tokens: McpTokenWire[];
125
+ }
126
+
127
+ export interface McpTokenGetResponseData {
128
+ token: McpTokenWire;
129
+ }
130
+
131
+ export interface McpTokenCreatedResponseData {
132
+ token: McpTokenCreatedWire;
133
+ }