@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.
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @egox/contracts — `/egox/tenants/*` wire types (project CRUD).
3
+ *
4
+ * `Tenant` is what the Console calls "Project" — same row, two names.
5
+ * The wire shape preserves `snake_case` field names because that's the
6
+ * actual JSON the backend has always emitted and the console has always
7
+ * consumed. Renaming to camelCase here would be a breaking wire change
8
+ * (every API caller would need to update) and is deliberately out of
9
+ * scope for the additive Phase 9 sweep. If/when we ever do that
10
+ * migration, this is the file the major-version bump touches.
11
+ *
12
+ * Drift cleaned up:
13
+ * - Backend `Tenant` used `Date` for `created_at` / `updated_at` /
14
+ * `attestation_signed_at`; wire and console already used ISO
15
+ * strings. Backend `formatTenant` now emits ISO via the same
16
+ * pattern as Tools and RAG.
17
+ */
18
+
19
+ // ============================================================================
20
+ // Locked vocabularies (mirror DB CHECK constraints)
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Compliance tier (mirrors `egox_tenants.compliance_tier` CHECK
25
+ * constraint, migration 010). Phase 1 services `'standard'` only;
26
+ * the rest are forward-compat for Phase 2.
27
+ */
28
+ export type ComplianceTier =
29
+ | 'standard'
30
+ | 'pending_review'
31
+ | 'gdpr'
32
+ | 'hipaa'
33
+ | 'enterprise'
34
+ | 'pending_upgrade';
35
+
36
+ /**
37
+ * Data residency region (mirrors `egox_tenants.data_residency_region`
38
+ * CHECK constraint, migration 010). Phase 1 deploys only to
39
+ * `'eu-west'`; the rest are forward-compat.
40
+ */
41
+ export type DataResidencyRegion =
42
+ | 'us-east'
43
+ | 'us-west'
44
+ | 'eu-west'
45
+ | 'eu-central'
46
+ | 'ap-south'
47
+ | 'ap-east';
48
+
49
+ /**
50
+ * Operational tenant status (mirrors `egox_tenants.status` CHECK
51
+ * constraint). The legacy `is_active` boolean still exists in
52
+ * Phase 1 and is NOT auto-synced with `status` — both fields are
53
+ * independent until Phase 1.5 deprecates `is_active`.
54
+ */
55
+ export type TenantStatus =
56
+ | 'active'
57
+ | 'pending_review'
58
+ | 'flagged'
59
+ | 'suspended'
60
+ | 'terminated';
61
+
62
+ /**
63
+ * Billing plan (Phase 1.5, two-plan model — migration 035). Drives usage
64
+ * quotas (e.g. monthly /ask limit).
65
+ *
66
+ * - `id_ego` — "Id Ego": free, pay-as-you-go via BYOK, capped limits.
67
+ * Default for self-service signup.
68
+ * - `superego` — "Superego": $20/mo, higher (but still capped) limits.
69
+ */
70
+ export type TenantPlan = 'id_ego' | 'superego';
71
+
72
+ /**
73
+ * Industry classification driving the signup gate (Phase 1.5+).
74
+ * The regulated entries (`'healthcare'`, `'finance'`, `'insurance'`,
75
+ * `'government'`, `'legal'`) auto-route new tenants to
76
+ * `status: 'pending_review'`.
77
+ */
78
+ export type TenantIndustry =
79
+ | 'saas'
80
+ | 'ecommerce'
81
+ | 'marketing'
82
+ | 'education'
83
+ | 'media'
84
+ | 'travel'
85
+ | 'real_estate'
86
+ | 'productivity'
87
+ | 'other'
88
+ | 'healthcare'
89
+ | 'finance'
90
+ | 'insurance'
91
+ | 'government'
92
+ | 'legal';
93
+
94
+ // ============================================================================
95
+ // TenantWire — the canonical row shape (snake_case on the wire)
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Wire-format tenant row. Returned by every `/egox/tenants/*` response.
100
+ *
101
+ * **Field naming.** This is the only wire shape in the contracts
102
+ * package that uses `snake_case`. It's preserved verbatim from the
103
+ * legacy backend → JSON path because the Console already consumes
104
+ * `snake_case` here too. Treat the inconsistency as fixed-by-history,
105
+ * not a goal — net change would be breaking, with zero behavioural
106
+ * value.
107
+ */
108
+ export interface TenantWire {
109
+ id: string;
110
+ name: string;
111
+ /** URL-safe slug, unique per parent (when `parent_tenant_id` is set). */
112
+ slug: string;
113
+ description?: string;
114
+ is_active: boolean;
115
+ /** ISO 8601 timestamp. */
116
+ created_at: string;
117
+ /** ISO 8601 timestamp. */
118
+ updated_at: string;
119
+
120
+ // Phase 1 compliance posture (migration 010) ----------------------
121
+ compliance_tier: ComplianceTier;
122
+ data_residency_region: DataResidencyRegion;
123
+ /**
124
+ * UUID of the parent tenant when this row is a sub-project. `null`
125
+ * for top-level tenants. Sub-projects share their parent's
126
+ * `compliance_tier` for billing purposes (enforced at the service
127
+ * layer, not the DB).
128
+ */
129
+ parent_tenant_id: string | null;
130
+ industry: TenantIndustry | null;
131
+ /** ISO 8601 timestamp. `null` until the operator signs an attestation. */
132
+ attestation_signed_at: string | null;
133
+ attestation_version: string | null;
134
+ status: TenantStatus;
135
+ /** Billing plan (migrations 023/035). Drives usage quotas. Default 'id_ego'. */
136
+ plan: TenantPlan;
137
+ }
138
+
139
+ // ============================================================================
140
+ // Request bodies (what the client SENDS)
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Wire body of `POST /egox/tenants`.
145
+ *
146
+ * Notable absences:
147
+ * - `compliance_tier`, `status` — server-derived from `industry`
148
+ * at signup (Phase 1C / Strategy A).
149
+ * Non-regulated industry →
150
+ * `'standard'` + `'active'`;
151
+ * regulated industry (healthcare,
152
+ * finance, insurance, government,
153
+ * legal) → `'pending_review'` on
154
+ * both. Manual tier moves stay an
155
+ * admin operation via
156
+ * `TenantUpdateBody`.
157
+ * - `is_active`, timestamps — server-managed.
158
+ */
159
+ export interface TenantCreateBody {
160
+ name: string;
161
+ slug?: string;
162
+ description?: string;
163
+
164
+ industry?: TenantIndustry;
165
+ data_residency_region?: DataResidencyRegion;
166
+ parent_tenant_id?: string | null;
167
+ /** ISO 8601 timestamp string the operator submits at signup. */
168
+ attestation_signed_at?: string;
169
+ attestation_version?: string;
170
+ }
171
+
172
+ /**
173
+ * Wire body of `PUT /egox/tenants/:tenantId`. Every field is optional —
174
+ * the backend applies `COALESCE`-style partial updates. `tenantId` is
175
+ * the URL param, not the body.
176
+ *
177
+ * `compliance_tier` and `status` are admin operations; Phase 1 has no
178
+ * role gating on these — Phase 1.5 will introduce admin-only
179
+ * enforcement.
180
+ */
181
+ export interface TenantUpdateBody {
182
+ name?: string;
183
+ description?: string;
184
+ is_active?: boolean;
185
+
186
+ industry?: TenantIndustry;
187
+ data_residency_region?: DataResidencyRegion;
188
+ parent_tenant_id?: string | null;
189
+ compliance_tier?: ComplianceTier;
190
+ status?: TenantStatus;
191
+ attestation_signed_at?: string;
192
+ attestation_version?: string;
193
+ }
194
+
195
+ // ============================================================================
196
+ // Response data shapes (carried inside `EgoxApiResponse.data`)
197
+ // ============================================================================
198
+
199
+ export interface TenantListResponseData {
200
+ tenants: TenantWire[];
201
+ }
202
+
203
+ export interface TenantGetResponseData {
204
+ tenant: TenantWire;
205
+ }
206
+
207
+ /**
208
+ * Returned by `POST /egox/tenants`. Wrapping the row in `tenant`
209
+ * (rather than returning it raw) matches the rest of the create
210
+ * surfaces and keeps room for additive metadata (creation source,
211
+ * downstream side effects, …) without a major version bump.
212
+ */
213
+ export interface TenantCreatedResponseData {
214
+ tenant: TenantWire;
215
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @egox/contracts — `/egox/threads/*` and
3
+ * `/egox/tenants/:tenantId/threads/*` wire types.
4
+ *
5
+ * The conversation memory surface. `Thread` rows are produced by every
6
+ * `/ask` turn (via `ThreadService.getOrCreateThread`) and read back
7
+ * via the admin threads list / detail routes.
8
+ *
9
+ * Phase 6's `externalThreadId` already lives in `AskRequestBody` /
10
+ * `AskResponseData` / `AskStreamDoneEvent` (in `ask.d.ts` /
11
+ * `stream.d.ts`). This file lifts the FULL `Thread` and `Message`
12
+ * row shapes — what the threads-admin endpoints return — into the
13
+ * contracts package so the console mirror can't drift.
14
+ *
15
+ * Drift cleaned up:
16
+ * - Backend `Thread` and `Message` used `Date` for `createdAt` /
17
+ * `updatedAt`; wire and console already used ISO strings.
18
+ * `formatThread` and `formatMessage` now emit ISO to match.
19
+ */
20
+
21
+ import type { Intent } from './envelope';
22
+
23
+ // ============================================================================
24
+ // Locked vocabularies
25
+ // ============================================================================
26
+
27
+ /** Role of a message in the conversation turn. */
28
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
29
+
30
+ /**
31
+ * Why a turn was stamped with `failedRecovery: true` by the
32
+ * orchestrator. Mirrors the CHECK constraint on
33
+ * `egox_messages.failure_reason` (migration 014, Phase 5 / Layer 5).
34
+ * Adding a new entry requires a migration + orchestrator change in
35
+ * lockstep — the audit field exists to be queryable, not free-form.
36
+ */
37
+ export type MessageFailureReason =
38
+ | 'tool_retry_storm'
39
+ | 'iteration_cap'
40
+ | 'max_calls_per_turn'
41
+ | 'unrecoverable_error';
42
+
43
+ // ============================================================================
44
+ // Supporting shapes
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Tool-call request emitted by an `assistant` message. Shape matches
49
+ * the OpenAI Chat Completions tool-call envelope verbatim — that's
50
+ * what the orchestrator persists. `arguments` is a JSON string the
51
+ * caller must `JSON.parse` if they want the structured value;
52
+ * EgoX doesn't decode it on the wire because it's already
53
+ * round-tripped through the LLM as text.
54
+ */
55
+ export interface ToolCallWire {
56
+ id: string;
57
+ type: 'function';
58
+ function: {
59
+ name: string;
60
+ arguments: string;
61
+ };
62
+ }
63
+
64
+ // ============================================================================
65
+ // ThreadWire — the canonical row shape
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Wire-format thread row. Returned by every threads-admin response and
70
+ * embedded in `ThreadWithMessagesWire`.
71
+ *
72
+ * `externalThreadId` is populated when the caller supplied one on the
73
+ * `/ask` that minted the row (Phase 6); `null` for legacy threads and
74
+ * for any caller that uses only the EgoX UUID path.
75
+ */
76
+ export interface ThreadWire {
77
+ id: string;
78
+ tenantId: string;
79
+ externalUserId: string | null;
80
+ /**
81
+ * Consumer-supplied conversation identifier (Phase 6). `null` for
82
+ * threads created before the column existed and for any caller
83
+ * that didn't supply one. Uniqueness is enforced per
84
+ * `(tenantId, externalThreadId)` among non-NULL rows by the
85
+ * `idx_egox_threads_tenant_ext` partial unique index — see
86
+ * migration 017.
87
+ */
88
+ externalThreadId: string | null;
89
+ title: string | null;
90
+ metadata: Record<string, unknown>;
91
+ isActive: boolean;
92
+ /** ISO 8601 timestamp. */
93
+ createdAt: string;
94
+ /** ISO 8601 timestamp. */
95
+ updatedAt: string;
96
+ }
97
+
98
+ /**
99
+ * Thread row plus aggregate stats. Returned by the admin threads list
100
+ * endpoint so the operator-facing table can sort by activity without
101
+ * an N+1 fetch.
102
+ */
103
+ export interface ThreadWithStatsWire extends ThreadWire {
104
+ messageCount: number;
105
+ totalTokens: number;
106
+ }
107
+
108
+ // ============================================================================
109
+ // MessageWire — the canonical message-row shape
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Wire-format message row. Carries the full per-turn audit trail —
114
+ * the LLM iteration's prompt/completion token split, the model id,
115
+ * the intent classification, the RAG context that was injected, and
116
+ * the Phase 5 failure-recovery flags.
117
+ *
118
+ * `toolCalls` is populated only on `role: 'assistant'` rows that
119
+ * requested a tool. `toolCallId` + `toolName` are populated only on
120
+ * `role: 'tool'` result rows.
121
+ */
122
+ export interface MessageWire {
123
+ id: string;
124
+ threadId: string;
125
+ tenantId: string;
126
+ role: MessageRole;
127
+ content: string | null;
128
+ toolCalls: ToolCallWire[] | null;
129
+ toolCallId: string | null;
130
+ toolName: string | null;
131
+ intent: Intent | null;
132
+ ragContext: Record<string, unknown> | null;
133
+ promptTokens: number | null;
134
+ completionTokens: number | null;
135
+ totalTokens: number | null;
136
+ model: string | null;
137
+ /** Echo of `AskRequestBody.metadata` for audit. */
138
+ metadata: Record<string, unknown> | null;
139
+ /**
140
+ * `true` when this row's `content` was deliberately dropped under
141
+ * no-store (metadata-only) persistence — see
142
+ * `AskRequestBody.noStore` and the tenant's `persistMessageContent`
143
+ * setting (#5). Distinguishes an intentional NULL (the conversation
144
+ * text was never stored) from an incidental empty turn. Always
145
+ * `false` for tenants that persist content normally.
146
+ */
147
+ contentRedacted: boolean;
148
+ /**
149
+ * `true` when this turn triggered a Phase 5 tool-retry-storm
150
+ * backstop (dedup ≥ MAX_DUPLICATE_CALLS_BEFORE_FLAG / per-tool
151
+ * cap / outer iteration cap) or ended on an unrecoverable error
152
+ * class. Always `false` for non-assistant rows. Phase 5 /
153
+ * Layer 5 — drives the Tool Health "bad turns" filter.
154
+ */
155
+ failedRecovery: boolean;
156
+ /**
157
+ * Dominant cause when `failedRecovery === true`. `null`
158
+ * otherwise. See `MessageFailureReason` for the locked
159
+ * vocabulary.
160
+ */
161
+ failureReason: MessageFailureReason | null;
162
+ /** ISO 8601 timestamp. */
163
+ createdAt: string;
164
+ }
165
+
166
+ /**
167
+ * Returned by the thread-detail endpoint. The full conversation —
168
+ * row + every message in chronological order.
169
+ */
170
+ export interface ThreadWithMessagesWire {
171
+ thread: ThreadWire;
172
+ messages: MessageWire[];
173
+ }
174
+
175
+ // ============================================================================
176
+ // Response data shapes (carried inside `EgoxApiResponse.data`)
177
+ // ============================================================================
178
+
179
+ export interface ThreadListResponseData {
180
+ threads: ThreadWithStatsWire[];
181
+ }
182
+
183
+ export interface ThreadGetResponseData extends ThreadWithMessagesWire {}
package/src/tool.d.ts ADDED
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @egox/contracts — `/egox/tools/*` + `/egox/mcp-admin/tools/*` wire types.
3
+ *
4
+ * Three independent consumers used to hand-mirror these shapes:
5
+ * - the backend (`modules/tool/interfaces.ts`)
6
+ * - the MCP server (`egox-mcp/src/backend-client.ts` — `BackendTool`)
7
+ * - the console (`console/src/types/models.ts` — `Tool`)
8
+ * Each drift was one rename away from a silent runtime bug — the same
9
+ * shape of failure that produced the `answer === undefined` regression
10
+ * on `/ask`. Phase 9.1 unifies them under `ToolWire`.
11
+ *
12
+ * **Wire shape vs. backend in-process shape.** Timestamps are typed as
13
+ * `string` here because that's what JSON serialization produces (Fastify
14
+ * calls `Date.prototype.toJSON()` on the way out). The backend's
15
+ * in-process `Tool` type re-exports `ToolWire` and its formatter now
16
+ * emits ISO strings to match — no consumer of `tool.createdAt` in the
17
+ * backend treats it as a `Date`, so there's no plumbing cost.
18
+ */
19
+
20
+ // ============================================================================
21
+ // Locked vocabularies
22
+ // ============================================================================
23
+
24
+ /**
25
+ * What kind of upstream API this tool wraps. Locked because the
26
+ * orchestrator branches on it (REST → fetch, GRAPHQL → POST gql_query).
27
+ * Adding a value is a one-edit change across backend + MCP + console
28
+ * because they all reference `ToolType` from this file.
29
+ */
30
+ export type ToolType = 'REST' | 'GRAPHQL';
31
+
32
+ /** HTTP method for REST tools. Locked — the backend allows only these. */
33
+ export type ToolHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
34
+
35
+ /**
36
+ * How the tool authenticates upstream.
37
+ * - `'FORWARD'` — EgoX injects the caller's `authToken` into the
38
+ * outbound request as `Authorization: Bearer …`.
39
+ * - `'NONE'` — no auth header is injected; tool-static `headers`
40
+ * are still applied.
41
+ */
42
+ export type ToolAuthType = 'FORWARD' | 'NONE';
43
+
44
+ /**
45
+ * Provenance of the tool definition.
46
+ * - `'manual'` — created in the Console.
47
+ * - `'mcp'` — created via the MCP server (Cursor / 3rd-party MCP
48
+ * client). Mutating an `mcp` tool from the Console
49
+ * requires an explicit override.
50
+ */
51
+ export type ToolSource = 'manual' | 'mcp';
52
+
53
+ // ============================================================================
54
+ // JSON Schema (recursive, for tool input/output shapes)
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Recursive JSON Schema fragment used by tool `inputSchema` /
59
+ * `outputSchema`. Distinct from `JsonSchemaDefinition` in `envelope.d.ts`
60
+ * (which is the narrow root-must-be-object shape used for `/ask`'s
61
+ * `responseFormat: 'json_object'`).
62
+ *
63
+ * Intentionally narrow vs. the full JSON Schema 2020-12 surface — only
64
+ * what EgoX's LLM-side adapter actually consumes. Adding a field here
65
+ * requires updating the adapter in `modules/tool/index.ts` in the
66
+ * same PR.
67
+ */
68
+ export interface JsonSchemaWire {
69
+ type: string;
70
+ properties?: Record<string, JsonSchemaPropertyWire>;
71
+ required?: string[];
72
+ additionalProperties?: boolean;
73
+ items?: JsonSchemaWire;
74
+ description?: string;
75
+ }
76
+
77
+ export interface JsonSchemaPropertyWire {
78
+ type: string;
79
+ description?: string;
80
+ enum?: string[];
81
+ items?: JsonSchemaPropertyWire;
82
+ properties?: Record<string, JsonSchemaPropertyWire>;
83
+ required?: string[];
84
+ }
85
+
86
+ // ============================================================================
87
+ // ToolWire — the canonical row shape
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Wire-format Tool row. Returned by every `/egox/tools/*` and
92
+ * `/egox/mcp-admin/tools/*` response that produces a Tool.
93
+ *
94
+ * Backend in-process `Tool` is an alias of this type — including the
95
+ * `tenantId` field, which is on the row even though the API caller
96
+ * never sends it (it's derived from API key / JWT context on the
97
+ * request side, but echoed on responses for audit).
98
+ */
99
+ export interface ToolWire {
100
+ id: string;
101
+ /**
102
+ * Owning tenant. Present on every response for audit; the request
103
+ * side never carries it (the backend derives tenant from the API
104
+ * key context).
105
+ */
106
+ tenantId: string;
107
+ toolName: string;
108
+ toolDescription: string;
109
+ apiEndpoint: string;
110
+ toolType: ToolType;
111
+ httpMethod: ToolHttpMethod;
112
+ inputSchema: JsonSchemaWire;
113
+ outputSchema: JsonSchemaWire;
114
+ headers: Record<string, string>;
115
+ authType: ToolAuthType;
116
+ retryAttempts: number;
117
+ retryDelayMs: number;
118
+ enabled: boolean;
119
+ /** GraphQL query/mutation string. Required when `toolType === 'GRAPHQL'`. */
120
+ gqlQuery: string | null;
121
+ /** Example input values for LLM reference. */
122
+ exampleInput: Record<string, unknown> | null;
123
+ /** Example output values for LLM reference. */
124
+ exampleOutput: Record<string, unknown> | null;
125
+ /** Total LLM-initiated invocations (denormalized counter). */
126
+ callCount: number;
127
+ successCount: number;
128
+ failureCount: number;
129
+ /**
130
+ * ISO 8601 timestamp of the most recent invocation. `null` when the
131
+ * tool has never been called. The backend in-process formatter
132
+ * emits ISO strings here so wire and runtime shapes agree.
133
+ */
134
+ lastCalledAt: string | null;
135
+ source: ToolSource;
136
+ /**
137
+ * Lowercase header-name allowlist. Headers from the inbound `/ask`
138
+ * request whose name matches an entry are forwarded verbatim to
139
+ * the tool endpoint. Empty array (default) = no inbound header is
140
+ * forwarded — preserves the original safe behavior.
141
+ *
142
+ * Forbidden names (`Cookie`, `Host`, `Authorization`, …) are
143
+ * stripped defensively at execution time even if accidentally
144
+ * listed here.
145
+ */
146
+ forwardHeaders: string[];
147
+ /**
148
+ * Hard cap on how many times this tool may be EXECUTED inside a
149
+ * single `/ask` turn. Dedup cache hits don't count — only real
150
+ * upstream calls. When exceeded, the orchestrator returns a
151
+ * synthetic refusal message to the LLM. Range 1-50, default 5
152
+ * (Phase 5 / Layer 2).
153
+ */
154
+ maxCallsPerTurn: number;
155
+ /** ISO 8601 timestamp. */
156
+ createdAt: string;
157
+ /** ISO 8601 timestamp. */
158
+ updatedAt: string;
159
+ }
160
+
161
+ // ============================================================================
162
+ // Request bodies (what the client SENDS)
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Wire body of `POST /egox/tools`. Notable absences:
167
+ * - `tenantId` — derived from API key context / JWT.
168
+ * - `enabled` — new tools are always created `enabled: true`.
169
+ * - counters / `lastCalledAt` — server-managed.
170
+ * - timestamps — server-managed.
171
+ */
172
+ export interface ToolCreateBody {
173
+ toolName: string;
174
+ toolDescription: string;
175
+ apiEndpoint: string;
176
+ toolType?: ToolType;
177
+ httpMethod?: ToolHttpMethod;
178
+ inputSchema: JsonSchemaWire;
179
+ outputSchema?: JsonSchemaWire;
180
+ headers?: Record<string, string>;
181
+ authType?: ToolAuthType;
182
+ retryAttempts?: number;
183
+ retryDelayMs?: number;
184
+ /** GraphQL query/mutation. Required when `toolType === 'GRAPHQL'`. */
185
+ gqlQuery?: string;
186
+ exampleInput?: Record<string, unknown>;
187
+ exampleOutput?: Record<string, unknown>;
188
+ /** Provenance. Defaults to `'manual'`. */
189
+ source?: ToolSource;
190
+ forwardHeaders?: string[];
191
+ /** Per-turn execution cap (1-50, default 5). */
192
+ maxCallsPerTurn?: number;
193
+ }
194
+
195
+ /**
196
+ * Wire body of `PUT /egox/tools/:toolId`. Every field is optional —
197
+ * unspecified fields are left untouched (`COALESCE` semantics).
198
+ * `toolId` lives in the URL, not the body; `tenantId` is derived
199
+ * from the auth context.
200
+ */
201
+ export interface ToolUpdateBody {
202
+ toolName?: string;
203
+ toolDescription?: string;
204
+ apiEndpoint?: string;
205
+ toolType?: ToolType;
206
+ httpMethod?: ToolHttpMethod;
207
+ inputSchema?: JsonSchemaWire;
208
+ outputSchema?: JsonSchemaWire;
209
+ headers?: Record<string, string>;
210
+ authType?: ToolAuthType;
211
+ retryAttempts?: number;
212
+ retryDelayMs?: number;
213
+ enabled?: boolean;
214
+ gqlQuery?: string;
215
+ exampleInput?: Record<string, unknown>;
216
+ exampleOutput?: Record<string, unknown>;
217
+ source?: ToolSource;
218
+ forwardHeaders?: string[];
219
+ maxCallsPerTurn?: number;
220
+ }
221
+
222
+ // ============================================================================
223
+ // Response data shapes (carried inside `EgoxApiResponse.data`)
224
+ // ============================================================================
225
+
226
+ export interface ToolListResponseData {
227
+ tools: ToolWire[];
228
+ }
229
+
230
+ export interface ToolGetResponseData {
231
+ tool: ToolWire;
232
+ }
233
+
234
+ // ============================================================================
235
+ // MCP-admin tool preview (`POST /egox/mcp-admin/tools/:name/preview`)
236
+ // ============================================================================
237
+
238
+ /**
239
+ * Single field diff entry for a tool-upsert preview. `oldValue` is
240
+ * `undefined` on a fresh create.
241
+ */
242
+ export interface ToolPreviewDiffEntry {
243
+ field: string;
244
+ oldValue: unknown;
245
+ newValue: unknown;
246
+ }
247
+
248
+ /**
249
+ * Response payload of the MCP "preview before upsert" endpoint.
250
+ *
251
+ * - `action` — what an unforced PUT would do.
252
+ * - `requiresForce` — true when overwriting a `'manual'` tool
253
+ * from MCP; the upsert refuses without
254
+ * `force: true`.
255
+ * - `existingSource` — provenance of the row that would be
256
+ * overwritten; null on a fresh create.
257
+ * - `validationErrors` — flat strings; empty array means the body
258
+ * is valid as-is.
259
+ * - `diff` — only the fields that would change.
260
+ */
261
+ export interface ToolPreview {
262
+ toolName: string;
263
+ action: 'create' | 'update' | 'noop';
264
+ exists: boolean;
265
+ requiresForce: boolean;
266
+ existingSource: ToolSource | null;
267
+ validationErrors: string[];
268
+ diff: ToolPreviewDiffEntry[];
269
+ }