@agentforge-io/sdk-server 4.0.18
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/dist/client.d.ts +72 -0
- package/dist/client.js +153 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +36 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +53 -0
- package/dist/resources/agents.d.ts +88 -0
- package/dist/resources/agents.js +119 -0
- package/dist/resources/chat.d.ts +55 -0
- package/dist/resources/chat.js +180 -0
- package/dist/resources/connectors.d.ts +200 -0
- package/dist/resources/connectors.js +192 -0
- package/dist/resources/secrets.d.ts +44 -0
- package/dist/resources/secrets.js +71 -0
- package/dist/resources/tools.d.ts +58 -0
- package/dist/resources/tools.js +72 -0
- package/dist/types.d.ts +355 -0
- package/dist/types.js +8 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.js +90 -0
- package/package.json +39 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// secrets resource — gobernanza de los secrets COMPANY-scoped del tenant
|
|
3
|
+
// desde una app externa (ScrappyDoc). Espeja el patrón de connectors: el
|
|
4
|
+
// tenant sale SIEMPRE del ak_live_, la app hija decide qué valores custom
|
|
5
|
+
// setear/rotar/borrar para su propio workspace, y el platform los guarda
|
|
6
|
+
// cifrados en `af_platform_secrets` (scope 'company').
|
|
7
|
+
//
|
|
8
|
+
// Para qué sirve: un tenant puede sobrescribir keys o crear valores custom
|
|
9
|
+
// (API keys de terceros, tokens, flags) que luego consumen sus agents/tools
|
|
10
|
+
// en runtime — sin que la app hija tenga que guardarlos ella misma. La
|
|
11
|
+
// plataforma madre es la única fuente de verdad y los sirve cifrados.
|
|
12
|
+
//
|
|
13
|
+
// Auth: SIEMPRE ak_live_ tenant-wide — estos endpoints usan `ApiKeyGuard` en
|
|
14
|
+
// el server (ver ApiKeySecretsController) y rechazan ChatToken, aunque el
|
|
15
|
+
// `AgentForgeClient` también tenga uno configurado (ver `requestWithApiKey`).
|
|
16
|
+
//
|
|
17
|
+
// SEGURIDAD: esta superficie SOLO ve/toca los secrets 'company' del tenant.
|
|
18
|
+
// Los secrets 'instance' de la plataforma (ANTHROPIC_API_KEY, STRIPE_*, OAuth
|
|
19
|
+
// de plataforma) NUNCA se exponen acá — el server fuerza scope 'company'.
|
|
20
|
+
// Los valores en claro NUNCA viajan de vuelta: `list()` solo devuelve un hint
|
|
21
|
+
// (últimos 4 chars) + metadata.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.SecretsResource = void 0;
|
|
24
|
+
class SecretsResource {
|
|
25
|
+
constructor(client) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `GET /tenant-secrets` — metadata de TODOS los secrets company-scoped que
|
|
30
|
+
* este tenant ha configurado. Nunca devuelve el valor en claro, solo
|
|
31
|
+
* `valueHint` (últimos 4 chars) y flags — suficiente para renderizar una
|
|
32
|
+
* lista "configurado / no configurado" en la UI de la app hija.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const { items } = await af.secrets.list();
|
|
36
|
+
* const configured = items.filter((s) => s.configured);
|
|
37
|
+
*/
|
|
38
|
+
async list() {
|
|
39
|
+
return this.client.requestWithApiKey('GET', '/tenant-secrets');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `PUT /tenant-secrets/:key` — crea o sobrescribe UN secret company-scoped
|
|
43
|
+
* para este tenant. El valor viaja cifrado al vault del platform; la
|
|
44
|
+
* respuesta trae solo metadata (nunca el valor en claro). La `key` debe
|
|
45
|
+
* matchear `/^[A-Z][A-Z0-9_]*$/` (mayúsculas, dígitos, guión bajo; empieza
|
|
46
|
+
* con letra) — el server responde 400 si no.
|
|
47
|
+
*
|
|
48
|
+
* El caller es responsable de gatear quién puede llamar esto (ej. solo
|
|
49
|
+
* admin/owner del workspace) — el server no impone rol propio más allá de
|
|
50
|
+
* la ak_live_ tenant-wide.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* await af.secrets.set('OPENAI_API_KEY', 'sk-...');
|
|
54
|
+
*/
|
|
55
|
+
async set(key, value) {
|
|
56
|
+
return this.client.requestWithApiKey('PUT', `/tenant-secrets/${encodeURIComponent(key)}`, { value });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `DELETE /tenant-secrets/:key` — borra el override company-scoped de este
|
|
60
|
+
* tenant para `key`. No toca el valor 'instance' de la plataforma (si lo
|
|
61
|
+
* hubiera): tras esto, la resolución en runtime cae al fallback de
|
|
62
|
+
* plataforma. Devuelve 204 (sin body).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* await af.secrets.remove('OPENAI_API_KEY');
|
|
66
|
+
*/
|
|
67
|
+
async remove(key) {
|
|
68
|
+
await this.client.requestWithApiKey('DELETE', `/tenant-secrets/${encodeURIComponent(key)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.SecretsResource = SecretsResource;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AgentForgeClient } from '../client';
|
|
2
|
+
import type { ToolCallbackPayload } from '../types';
|
|
3
|
+
export interface VerifyCallbackInput {
|
|
4
|
+
/** El body EXACTO tal como llegó por la red (antes de cualquier
|
|
5
|
+
* JSON.parse) — HMAC es sensible a cualquier diferencia de bytes. */
|
|
6
|
+
rawBody: string;
|
|
7
|
+
/** Valor crudo del header `X-AgentForge-Signature`. */
|
|
8
|
+
signatureHeader: string;
|
|
9
|
+
/** El secret `whsec_...` que recibiste al registrar esta tool. */
|
|
10
|
+
secretPlaintext: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RespondToCallbackInput {
|
|
13
|
+
/** El `resultUrl` que trajo el `ToolCallbackPayload` — path relativo,
|
|
14
|
+
* se resuelve contra el mismo `baseURL` del client. */
|
|
15
|
+
resultUrl: string;
|
|
16
|
+
tenantId: string;
|
|
17
|
+
/** El mismo secret `whsec_...` usado para verificar el callback entrante. */
|
|
18
|
+
secret: string;
|
|
19
|
+
/** Texto que el LLM verá como tool_result. Ignorado si `errorMessage` viene. */
|
|
20
|
+
result?: string;
|
|
21
|
+
/** Si la ejecución falló, el platform marca la ToolCall como failed y el
|
|
22
|
+
* LLM ve este mensaje como el resultado. */
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class ToolsResource {
|
|
26
|
+
private readonly client;
|
|
27
|
+
constructor(client: AgentForgeClient);
|
|
28
|
+
/**
|
|
29
|
+
* Verifica la firma HMAC de un callback ENTRANTE del platform y devuelve
|
|
30
|
+
* el payload ya parseado. Tira `ToolCallbackSignatureError` (código
|
|
31
|
+
* 'malformed' | 'stale' | 'mismatch') si la firma no es válida — tu
|
|
32
|
+
* endpoint debería responder 401 en ese caso, nunca ejecutar la tool.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const payload = af.tools.verifyCallback({
|
|
36
|
+
* rawBody: req.rawBody!.toString('utf8'),
|
|
37
|
+
* signatureHeader: req.headers['x-agentforge-signature'] as string,
|
|
38
|
+
* secretPlaintext: process.env.MY_TOOL_SECRET!,
|
|
39
|
+
* });
|
|
40
|
+
* // payload.toolName, payload.input, payload.resultUrl…
|
|
41
|
+
*/
|
|
42
|
+
verifyCallback(input: VerifyCallbackInput): ToolCallbackPayload;
|
|
43
|
+
/**
|
|
44
|
+
* Responde el resultado de una tool 'remote' ejecutada localmente —
|
|
45
|
+
* firma el body y lo POSTea a `resultUrl`. El platform reanuda el turno
|
|
46
|
+
* del LLM con ese resultado (o lo marca failed si mandaste
|
|
47
|
+
* `errorMessage`). 204 en éxito — no hay nada que leer del response.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* await af.tools.respondToCallback({
|
|
51
|
+
* resultUrl: payload.resultUrl,
|
|
52
|
+
* tenantId: payload.tenantId,
|
|
53
|
+
* secret: process.env.MY_TOOL_SECRET!,
|
|
54
|
+
* result: JSON.stringify({ ok: true }),
|
|
55
|
+
* });
|
|
56
|
+
*/
|
|
57
|
+
respondToCallback(input: RespondToCallbackInput): Promise<void>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// tools resource — el lado CLIENTE del ciclo de vida de una tool 'remote'
|
|
3
|
+
// registrada vía `agents.registerTools({ tools: [{ executionMode: 'remote', ... }] })`:
|
|
4
|
+
//
|
|
5
|
+
// 1. El LLM invoca la tool → el platform POSTea (firmado) tu propio
|
|
6
|
+
// `callbackUrl` público con un `ToolCallbackPayload`.
|
|
7
|
+
// 2. Tu endpoint verifica la firma (`verifyCallback`) y ejecuta la tool
|
|
8
|
+
// localmente — nunca corre en el platform.
|
|
9
|
+
// 3. Devolvés el resultado con `respondToCallback()`, firmado con el MISMO
|
|
10
|
+
// secret — el platform reanuda el turno del LLM con ese resultado.
|
|
11
|
+
//
|
|
12
|
+
// Este resource NO expone un servidor HTTP (eso lo monta tu framework — Nest,
|
|
13
|
+
// Express, lo que sea); solo la verificación/firma/POST del protocolo.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ToolsResource = void 0;
|
|
16
|
+
const webhooks_1 = require("../webhooks");
|
|
17
|
+
class ToolsResource {
|
|
18
|
+
constructor(client) {
|
|
19
|
+
this.client = client;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Verifica la firma HMAC de un callback ENTRANTE del platform y devuelve
|
|
23
|
+
* el payload ya parseado. Tira `ToolCallbackSignatureError` (código
|
|
24
|
+
* 'malformed' | 'stale' | 'mismatch') si la firma no es válida — tu
|
|
25
|
+
* endpoint debería responder 401 en ese caso, nunca ejecutar la tool.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const payload = af.tools.verifyCallback({
|
|
29
|
+
* rawBody: req.rawBody!.toString('utf8'),
|
|
30
|
+
* signatureHeader: req.headers['x-agentforge-signature'] as string,
|
|
31
|
+
* secretPlaintext: process.env.MY_TOOL_SECRET!,
|
|
32
|
+
* });
|
|
33
|
+
* // payload.toolName, payload.input, payload.resultUrl…
|
|
34
|
+
*/
|
|
35
|
+
verifyCallback(input) {
|
|
36
|
+
(0, webhooks_1.verifyToolCallbackSignature)({
|
|
37
|
+
rawBody: input.rawBody,
|
|
38
|
+
signatureHeader: input.signatureHeader,
|
|
39
|
+
secretPlaintext: input.secretPlaintext,
|
|
40
|
+
});
|
|
41
|
+
return JSON.parse(input.rawBody);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Responde el resultado de una tool 'remote' ejecutada localmente —
|
|
45
|
+
* firma el body y lo POSTea a `resultUrl`. El platform reanuda el turno
|
|
46
|
+
* del LLM con ese resultado (o lo marca failed si mandaste
|
|
47
|
+
* `errorMessage`). 204 en éxito — no hay nada que leer del response.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* await af.tools.respondToCallback({
|
|
51
|
+
* resultUrl: payload.resultUrl,
|
|
52
|
+
* tenantId: payload.tenantId,
|
|
53
|
+
* secret: process.env.MY_TOOL_SECRET!,
|
|
54
|
+
* result: JSON.stringify({ ok: true }),
|
|
55
|
+
* });
|
|
56
|
+
*/
|
|
57
|
+
async respondToCallback(input) {
|
|
58
|
+
const rawBody = JSON.stringify(input.errorMessage
|
|
59
|
+
? { result: '', errorMessage: input.errorMessage }
|
|
60
|
+
: { result: input.result ?? '' });
|
|
61
|
+
const signature = (0, webhooks_1.signToolCallbackPayload)(input.secret, rawBody);
|
|
62
|
+
await this.client.requestRaw('POST', input.resultUrl, {
|
|
63
|
+
rawBody,
|
|
64
|
+
headers: {
|
|
65
|
+
[webhooks_1.TOOL_CALLBACK_HEADERS.signature]: signature,
|
|
66
|
+
[webhooks_1.TOOL_CALLBACK_HEADERS.tenant]: input.tenantId,
|
|
67
|
+
},
|
|
68
|
+
timeoutMs: 10000,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.ToolsResource = ToolsResource;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
export type ToolPermissionMode = 'allow' | 'approval' | 'blocked';
|
|
2
|
+
export interface ToolPermission {
|
|
3
|
+
name: string;
|
|
4
|
+
mode: ToolPermissionMode;
|
|
5
|
+
}
|
|
6
|
+
export interface ExternalToolSummary {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: Record<string, unknown>;
|
|
10
|
+
executionMode: 'client' | 'remote';
|
|
11
|
+
/** Solo presente cuando `executionMode === 'remote'`. */
|
|
12
|
+
callbackUrl?: string | null;
|
|
13
|
+
/** Prefijo del whsec_ — útil para identificar qué secret tiene el cliente
|
|
14
|
+
* en su .env. El plaintext nunca se devuelve en el endpoint. */
|
|
15
|
+
secretPrefix?: string | null;
|
|
16
|
+
source: 'client-registered';
|
|
17
|
+
lastUpsertedAt: string | Date;
|
|
18
|
+
}
|
|
19
|
+
export interface McpServerConfig {
|
|
20
|
+
name: string;
|
|
21
|
+
transport: 'http' | 'sse';
|
|
22
|
+
url: string;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
tenantId?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AgentMe {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
model: string | null;
|
|
32
|
+
systemPrompt: string;
|
|
33
|
+
context: string | null;
|
|
34
|
+
temperature: number | null;
|
|
35
|
+
topP: number | null;
|
|
36
|
+
maxTokens: number | null;
|
|
37
|
+
tools: ToolPermission[];
|
|
38
|
+
mcpServers: McpServerConfig[];
|
|
39
|
+
skills: string[];
|
|
40
|
+
timezone: string | null;
|
|
41
|
+
isActive: boolean;
|
|
42
|
+
updatedAt: string | Date;
|
|
43
|
+
externalTools: ExternalToolSummary[];
|
|
44
|
+
}
|
|
45
|
+
export interface RegisterToolInput {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
inputSchema: Record<string, unknown>;
|
|
49
|
+
/** 'client' (default) = la app cliente ejecuta la tool in-process, solo
|
|
50
|
+
* declara el shape para visibilidad/auditoría. 'remote' = el platform
|
|
51
|
+
* invoca `callbackUrl` cuando el LLM la usa y espera el resultado en
|
|
52
|
+
* POST /api/v1/agents/me/tool-results/:toolUseId (ver ToolsResource). */
|
|
53
|
+
executionMode?: 'client' | 'remote';
|
|
54
|
+
/** Requerido cuando `executionMode === 'remote'`. */
|
|
55
|
+
callbackUrl?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface RegisterToolsInput {
|
|
58
|
+
tools: RegisterToolInput[];
|
|
59
|
+
}
|
|
60
|
+
export interface RegisterToolsResult {
|
|
61
|
+
agentId: string;
|
|
62
|
+
count: number;
|
|
63
|
+
tools: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
lastUpsertedAt: string | Date;
|
|
66
|
+
/** Solo presente cuando esta tool (o su callbackUrl-group entero, ver
|
|
67
|
+
* ExternalToolsRepository) acaba de rotar su secret HMAC — el
|
|
68
|
+
* plaintext SOLO se muestra esta vez, guárdalo. Ausente si el secret
|
|
69
|
+
* no cambió respecto al registro anterior. */
|
|
70
|
+
secret?: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
export interface AgentSummary {
|
|
74
|
+
id: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
name: string;
|
|
77
|
+
description: string | null;
|
|
78
|
+
model: string | null;
|
|
79
|
+
systemPrompt: string;
|
|
80
|
+
context: string | null;
|
|
81
|
+
temperature: number | null;
|
|
82
|
+
topP: number | null;
|
|
83
|
+
maxTokens: number | null;
|
|
84
|
+
timezone: string | null;
|
|
85
|
+
isActive: boolean;
|
|
86
|
+
createdAt: string | Date;
|
|
87
|
+
updatedAt: string | Date;
|
|
88
|
+
}
|
|
89
|
+
export interface CreateAgentInput {
|
|
90
|
+
slug: string;
|
|
91
|
+
name: string;
|
|
92
|
+
systemPrompt?: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
model?: string;
|
|
95
|
+
context?: string;
|
|
96
|
+
temperature?: number;
|
|
97
|
+
topP?: number;
|
|
98
|
+
maxTokens?: number;
|
|
99
|
+
timezone?: string;
|
|
100
|
+
isActive?: boolean;
|
|
101
|
+
metadata?: Record<string, unknown>;
|
|
102
|
+
tools?: Array<string | {
|
|
103
|
+
name: string;
|
|
104
|
+
mode: ToolPermissionMode;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
export interface UpdateAgentInput {
|
|
108
|
+
name?: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
model?: string;
|
|
111
|
+
systemPrompt?: string;
|
|
112
|
+
context?: string;
|
|
113
|
+
temperature?: number;
|
|
114
|
+
topP?: number;
|
|
115
|
+
maxTokens?: number;
|
|
116
|
+
timezone?: string;
|
|
117
|
+
isActive?: boolean;
|
|
118
|
+
metadata?: Record<string, unknown>;
|
|
119
|
+
/** Set completo de permisos de tool. Pass-through al backend — cada item
|
|
120
|
+
* es `{name, mode}` (a diferencia de `CreateAgentInput.tools`, que además
|
|
121
|
+
* acepta strings sueltos con mode='allow' implícito). */
|
|
122
|
+
tools?: Array<string | {
|
|
123
|
+
name: string;
|
|
124
|
+
mode: ToolPermissionMode;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export interface ListAgentsResult {
|
|
128
|
+
items: AgentSummary[];
|
|
129
|
+
total: number;
|
|
130
|
+
}
|
|
131
|
+
export interface ChatTokenRecord {
|
|
132
|
+
token: string;
|
|
133
|
+
tenantId: string;
|
|
134
|
+
agentId: string;
|
|
135
|
+
name: string;
|
|
136
|
+
allowedOrigins?: string[];
|
|
137
|
+
isActive: boolean;
|
|
138
|
+
createdByUserId?: string;
|
|
139
|
+
lastUsedAt?: string | Date;
|
|
140
|
+
createdAt: string | Date;
|
|
141
|
+
updatedAt: string | Date;
|
|
142
|
+
}
|
|
143
|
+
export type MessageRole = 'user' | 'assistant' | 'system';
|
|
144
|
+
export type ConversationStatus = 'active' | 'completed' | 'error' | 'paused';
|
|
145
|
+
export interface TokenUsage {
|
|
146
|
+
inputTokens: number;
|
|
147
|
+
outputTokens: number;
|
|
148
|
+
totalTokens: number;
|
|
149
|
+
cacheCreationInputTokens?: number;
|
|
150
|
+
cacheReadInputTokens?: number;
|
|
151
|
+
}
|
|
152
|
+
export interface ToolCallRecord {
|
|
153
|
+
toolName: string;
|
|
154
|
+
toolUseId: string;
|
|
155
|
+
input: Record<string, unknown>;
|
|
156
|
+
output?: string;
|
|
157
|
+
error?: string;
|
|
158
|
+
durationMs?: number;
|
|
159
|
+
}
|
|
160
|
+
export interface MessageRecord {
|
|
161
|
+
id: string;
|
|
162
|
+
role: MessageRole;
|
|
163
|
+
content: string;
|
|
164
|
+
toolCalls?: ToolCallRecord[];
|
|
165
|
+
usage?: TokenUsage;
|
|
166
|
+
metadata?: Record<string, unknown>;
|
|
167
|
+
createdAt: string | Date;
|
|
168
|
+
}
|
|
169
|
+
export interface ConversationHistory {
|
|
170
|
+
conversationId: string;
|
|
171
|
+
agentId: string;
|
|
172
|
+
userId: string;
|
|
173
|
+
title?: string;
|
|
174
|
+
status: ConversationStatus;
|
|
175
|
+
messages: MessageRecord[];
|
|
176
|
+
createdAt: string | Date;
|
|
177
|
+
updatedAt: string | Date;
|
|
178
|
+
}
|
|
179
|
+
export interface ApprovalCopyBundle {
|
|
180
|
+
title: string;
|
|
181
|
+
body: string;
|
|
182
|
+
approveLabel: string;
|
|
183
|
+
approveBusyLabel: string;
|
|
184
|
+
denyLabel: string;
|
|
185
|
+
denyBusyLabel: string;
|
|
186
|
+
approvedPill: string;
|
|
187
|
+
deniedPill: string;
|
|
188
|
+
readOnlyHint: string;
|
|
189
|
+
blockedTitle: string;
|
|
190
|
+
blockedBody: string;
|
|
191
|
+
expiresPrefix: string;
|
|
192
|
+
}
|
|
193
|
+
interface ActingAgentTag {
|
|
194
|
+
/** Presente cuando un team orchestrator delegó el turno a un miembro —
|
|
195
|
+
* el id de quién habla. Ausente = el agent primario de la sesión. */
|
|
196
|
+
actingAgentId?: string;
|
|
197
|
+
}
|
|
198
|
+
/** Unión completa de chunks que puede traer un turno de chat-stream. */
|
|
199
|
+
export type ChatStreamChunk = ({
|
|
200
|
+
type: 'text_delta';
|
|
201
|
+
delta: string;
|
|
202
|
+
} & ActingAgentTag) | ({
|
|
203
|
+
type: 'tool_use_start';
|
|
204
|
+
toolName: string;
|
|
205
|
+
toolUseId: string;
|
|
206
|
+
input?: Record<string, unknown>;
|
|
207
|
+
} & ActingAgentTag) | ({
|
|
208
|
+
type: 'tool_result';
|
|
209
|
+
toolName: string;
|
|
210
|
+
result: string;
|
|
211
|
+
} & ActingAgentTag) | ({
|
|
212
|
+
type: 'respond_data';
|
|
213
|
+
internalData?: unknown;
|
|
214
|
+
} & ActingAgentTag) | {
|
|
215
|
+
type: 'delegation_start';
|
|
216
|
+
subAgentId: string;
|
|
217
|
+
task: string;
|
|
218
|
+
subAgentName?: string;
|
|
219
|
+
subAgentAvatarUrl?: string;
|
|
220
|
+
} | {
|
|
221
|
+
type: 'delegation_result';
|
|
222
|
+
subAgentId: string;
|
|
223
|
+
result: string;
|
|
224
|
+
} | ({
|
|
225
|
+
type: 'usage';
|
|
226
|
+
usage: TokenUsage;
|
|
227
|
+
} & ActingAgentTag) | {
|
|
228
|
+
type: 'awaiting_approval';
|
|
229
|
+
approvalId: string;
|
|
230
|
+
toolName: string;
|
|
231
|
+
expiresAt: string;
|
|
232
|
+
connectorName?: string;
|
|
233
|
+
toolDescription?: string;
|
|
234
|
+
copy?: ApprovalCopyBundle;
|
|
235
|
+
} | {
|
|
236
|
+
type: 'tool_blocked';
|
|
237
|
+
toolName: string;
|
|
238
|
+
reason?: string;
|
|
239
|
+
connectorName?: string;
|
|
240
|
+
toolDescription?: string;
|
|
241
|
+
copy?: ApprovalCopyBundle;
|
|
242
|
+
} | {
|
|
243
|
+
type: 'done';
|
|
244
|
+
messageId: string;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Evento tipado que `ChatResource.streamMessage()` yieldea — envuelve el
|
|
248
|
+
* frame crudo SSE (`event:`/`data:`) que emite chat-stream.controller.ts:
|
|
249
|
+
* - `conversation`: primer frame, siempre. Trae el conversationId (nuevo o
|
|
250
|
+
* el que pasaste) para poder persistirlo (ej. en la URL) desde el turno 1.
|
|
251
|
+
* - `chunk`: cualquier StreamChunk del turno del agente (incluye su propio
|
|
252
|
+
* `{type:'done', messageId}` cuando el agente terminó su respuesta).
|
|
253
|
+
* - `stream_end`: el frame `{ok:true}` que cierra la conexión HTTP —
|
|
254
|
+
* DISTINTO del `chunk` de tipo 'done' de arriba (el wire manda AMBOS,
|
|
255
|
+
* con el mismo `event: done` pero shapes distintos — se discriminan acá
|
|
256
|
+
* por la presencia de `.type` en el JSON).
|
|
257
|
+
* - `error`: el turno falló server-side (tool remota caída, LLM error, etc).
|
|
258
|
+
*/
|
|
259
|
+
export type ChatStreamEvent = {
|
|
260
|
+
kind: 'conversation';
|
|
261
|
+
conversationId: string;
|
|
262
|
+
} | {
|
|
263
|
+
kind: 'chunk';
|
|
264
|
+
chunk: ChatStreamChunk;
|
|
265
|
+
} | {
|
|
266
|
+
kind: 'stream_end';
|
|
267
|
+
ok: boolean;
|
|
268
|
+
} | {
|
|
269
|
+
kind: 'error';
|
|
270
|
+
message: string;
|
|
271
|
+
};
|
|
272
|
+
export interface ConnectorStatus {
|
|
273
|
+
connectorId: string;
|
|
274
|
+
/** ¿El tenant ya completó el OAuth/API-key setup de este connector? */
|
|
275
|
+
connected: boolean;
|
|
276
|
+
/** ¿Tiene selector de recursos real implementado (nivel 2 del picker)?
|
|
277
|
+
* false = se puede conectar pero `listResources()` devuelve `unsupported`. */
|
|
278
|
+
browsable: boolean;
|
|
279
|
+
}
|
|
280
|
+
export interface ConnectorResourceItem {
|
|
281
|
+
id: string;
|
|
282
|
+
name: string;
|
|
283
|
+
description?: string;
|
|
284
|
+
}
|
|
285
|
+
export interface ListConnectorResourcesResult {
|
|
286
|
+
items: ConnectorResourceItem[];
|
|
287
|
+
/** true cuando el connector está conectado pero `browsable` es false —
|
|
288
|
+
* no es un error, la UI debe mostrar un estado "sin selector todavía". */
|
|
289
|
+
unsupported?: true;
|
|
290
|
+
}
|
|
291
|
+
export interface CreateClickupTaskInput {
|
|
292
|
+
/** Id de la lista de ClickUp (`ConnectorResourceItem.id` del picker). */
|
|
293
|
+
listId: string;
|
|
294
|
+
name: string;
|
|
295
|
+
description?: string;
|
|
296
|
+
}
|
|
297
|
+
export interface CreateClickupTaskResult {
|
|
298
|
+
id: string;
|
|
299
|
+
/** URL de la tarea en la app de ClickUp, para enlazar desde el caller. */
|
|
300
|
+
url: string;
|
|
301
|
+
name: string;
|
|
302
|
+
}
|
|
303
|
+
/** De dónde viene el valor resuelto: override guardado por el tenant,
|
|
304
|
+
* template del autor del connector, o fallback allow-all implícito. */
|
|
305
|
+
export type ConnectorDefaultsSource = 'tenant' | 'template' | 'fallback';
|
|
306
|
+
export interface ConnectorToolInfo {
|
|
307
|
+
name: string;
|
|
308
|
+
description?: string;
|
|
309
|
+
}
|
|
310
|
+
export interface ConnectorDefaultsResult {
|
|
311
|
+
tools: ConnectorToolInfo[];
|
|
312
|
+
permissions: ToolPermission[];
|
|
313
|
+
source: ConnectorDefaultsSource;
|
|
314
|
+
}
|
|
315
|
+
/** Body que el platform POSTea al `callbackUrl` de una tool 'remote' cuando
|
|
316
|
+
* el LLM la invoca. Firmado con HMAC (ver webhooks.ts) — usa
|
|
317
|
+
* `ToolsResource.verifyCallback()` para verificar + parsear en un paso. */
|
|
318
|
+
export interface ToolCallbackPayload {
|
|
319
|
+
toolUseId: string;
|
|
320
|
+
tenantId: string;
|
|
321
|
+
agentId: string;
|
|
322
|
+
conversationId: string;
|
|
323
|
+
toolName: string;
|
|
324
|
+
input: Record<string, unknown>;
|
|
325
|
+
/** ISO timestamp — el platform deja de esperar la respuesta después de
|
|
326
|
+
* esto (default 30s desde que se disparó el callback). */
|
|
327
|
+
expiresAt: string;
|
|
328
|
+
/** Path relativo (no absoluto) — pasalo tal cual a
|
|
329
|
+
* `ToolsResource.respondToCallback({ resultUrl, ... })`. */
|
|
330
|
+
resultUrl: string;
|
|
331
|
+
}
|
|
332
|
+
export interface IssueChatTokenInput {
|
|
333
|
+
/** Etiqueta humana. Si se omite, el server asigna un default con fecha. */
|
|
334
|
+
name?: string;
|
|
335
|
+
/** Si se omite, el token acepta cualquier Origin (útil en dev). */
|
|
336
|
+
allowedOrigins?: string[];
|
|
337
|
+
}
|
|
338
|
+
/** Metadata pública de un secret company-scoped del tenant, tal como la
|
|
339
|
+
* devuelve `af.secrets.list()` / `af.secrets.set()`. NUNCA incluye el valor
|
|
340
|
+
* en claro ni el ciphertext — solo un hint (últimos 4 chars) + flags. */
|
|
341
|
+
export interface TenantSecretEntry {
|
|
342
|
+
/** Nombre de la key (ej. `OPENAI_API_KEY`). */
|
|
343
|
+
key: string;
|
|
344
|
+
/** Descripción libre; el server pone un placeholder si no se dio ninguna. */
|
|
345
|
+
description: string;
|
|
346
|
+
/** `true` cuando el tenant tiene un valor guardado para esta key. */
|
|
347
|
+
configured: boolean;
|
|
348
|
+
/** Últimos 4 chars del valor (o `••••` si es muy corto). Ausente si no hay
|
|
349
|
+
* valor persistido. Nunca el valor completo. */
|
|
350
|
+
valueHint?: string;
|
|
351
|
+
/** ISO timestamp de la última escritura (serializado por el JSON transport).
|
|
352
|
+
* Ausente si nunca se configuró. */
|
|
353
|
+
updatedAt?: string;
|
|
354
|
+
}
|
|
355
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tipos compartidos del SDK. Espejan los shapes que devuelven los endpoints
|
|
3
|
+
// públicos del platform (ProxyPublicController). NO importamos del server
|
|
4
|
+
// para mantener el SDK desacoplado del runtime de NestJS.
|
|
5
|
+
//
|
|
6
|
+
// Cuando el server cambia un shape público, esto debe actualizarse. Eventual
|
|
7
|
+
// migración a generación automática desde OpenAPI queda como follow-up.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const TOOL_CALLBACK_HEADERS: {
|
|
2
|
+
readonly signature: "x-agentforge-signature";
|
|
3
|
+
readonly tenant: "x-agentforge-tenant";
|
|
4
|
+
};
|
|
5
|
+
export declare class ToolCallbackSignatureError extends Error {
|
|
6
|
+
readonly code: 'malformed' | 'stale' | 'mismatch';
|
|
7
|
+
constructor(code: 'malformed' | 'stale' | 'mismatch', message: string);
|
|
8
|
+
}
|
|
9
|
+
/** Firma un body para enviar al peer. Devuelve el valor del header
|
|
10
|
+
* `X-AgentForge-Signature`. */
|
|
11
|
+
export declare function signToolCallbackPayload(secretPlaintext: string, rawBody: string): string;
|
|
12
|
+
/** Verifica una firma contra el secret plaintext + raw body. Constant-time.
|
|
13
|
+
* Tira `ToolCallbackSignatureError` si la firma es inválida, vieja, o
|
|
14
|
+
* malformed. El `rawBody` debe ser EXACTAMENTE el string que llegó por la
|
|
15
|
+
* red (no un objeto re-serializado) — HMAC es sensible a cualquier byte de
|
|
16
|
+
* diferencia (orden de keys, espacios, etc). */
|
|
17
|
+
export declare function verifyToolCallbackSignature(input: {
|
|
18
|
+
rawBody: string;
|
|
19
|
+
signatureHeader: string;
|
|
20
|
+
secretPlaintext: string;
|
|
21
|
+
nowSeconds?: number;
|
|
22
|
+
}): void;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Helpers de HMAC para autenticación de callbacks Platform ↔ Cliente — MISMO
|
|
3
|
+
// algoritmo exacto que agentforge-platform/apps/server/src/modules/proxy/
|
|
4
|
+
// hmac.ts (portado acá, no importado, por la misma razón de desacople del
|
|
5
|
+
// resto del SDK: esta app cliente firma/verifica en AMBAS direcciones y
|
|
6
|
+
// necesita el algoritmo disponible sin arrastrar el runtime del server).
|
|
7
|
+
//
|
|
8
|
+
// Modelo (estilo Stripe webhook):
|
|
9
|
+
// - Cliente y platform comparten un secret HMAC `whsec_*` — el cliente lo
|
|
10
|
+
// recibe plaintext en `RegisterToolsResult.tools[].secret` (solo la
|
|
11
|
+
// primera vez, o cuando rota — ver AgentsResource.registerTools).
|
|
12
|
+
// - Cada mensaje firmado lleva header:
|
|
13
|
+
// X-AgentForge-Signature: t=<timestamp>,v1=<hex>
|
|
14
|
+
// donde:
|
|
15
|
+
// timestamp = unix segundos
|
|
16
|
+
// v1 = HMAC_SHA256(secret, `${timestamp}.${raw_body}`)
|
|
17
|
+
// - El receptor: extrae timestamp + v1, recomputa, compara constant-time.
|
|
18
|
+
// - Defensa contra replay: timestamp debe estar dentro de ±5 min (300 s).
|
|
19
|
+
//
|
|
20
|
+
// Direcciones de uso:
|
|
21
|
+
// - El platform FIRMA cuando POSTea el callback de una tool 'remote' al
|
|
22
|
+
// cliente — el cliente VERIFICA con `verifyToolCallbackSignature`.
|
|
23
|
+
// - El cliente FIRMA cuando responde con el resultado — ver
|
|
24
|
+
// `ToolsResource.respondToCallback`, que llama `signToolCallbackPayload`
|
|
25
|
+
// internamente (no hace falta invocarlo a mano).
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.ToolCallbackSignatureError = exports.TOOL_CALLBACK_HEADERS = void 0;
|
|
28
|
+
exports.signToolCallbackPayload = signToolCallbackPayload;
|
|
29
|
+
exports.verifyToolCallbackSignature = verifyToolCallbackSignature;
|
|
30
|
+
const node_crypto_1 = require("node:crypto");
|
|
31
|
+
const MAX_AGE_SECONDS = 300;
|
|
32
|
+
exports.TOOL_CALLBACK_HEADERS = {
|
|
33
|
+
signature: 'x-agentforge-signature',
|
|
34
|
+
tenant: 'x-agentforge-tenant',
|
|
35
|
+
};
|
|
36
|
+
class ToolCallbackSignatureError extends Error {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'ToolCallbackSignatureError';
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.ToolCallbackSignatureError = ToolCallbackSignatureError;
|
|
44
|
+
/** Firma un body para enviar al peer. Devuelve el valor del header
|
|
45
|
+
* `X-AgentForge-Signature`. */
|
|
46
|
+
function signToolCallbackPayload(secretPlaintext, rawBody) {
|
|
47
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
48
|
+
return `t=${ts},v1=${computeHmac(secretPlaintext, ts, rawBody)}`;
|
|
49
|
+
}
|
|
50
|
+
/** Verifica una firma contra el secret plaintext + raw body. Constant-time.
|
|
51
|
+
* Tira `ToolCallbackSignatureError` si la firma es inválida, vieja, o
|
|
52
|
+
* malformed. El `rawBody` debe ser EXACTAMENTE el string que llegó por la
|
|
53
|
+
* red (no un objeto re-serializado) — HMAC es sensible a cualquier byte de
|
|
54
|
+
* diferencia (orden de keys, espacios, etc). */
|
|
55
|
+
function verifyToolCallbackSignature(input) {
|
|
56
|
+
const parsed = parseSignatureHeader(input.signatureHeader);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
throw new ToolCallbackSignatureError('malformed', 'Signature header is malformed');
|
|
59
|
+
}
|
|
60
|
+
const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
61
|
+
if (Math.abs(now - parsed.timestamp) > MAX_AGE_SECONDS) {
|
|
62
|
+
throw new ToolCallbackSignatureError('stale', `Signature timestamp is more than ${MAX_AGE_SECONDS}s old or in the future`);
|
|
63
|
+
}
|
|
64
|
+
const expected = computeHmac(input.secretPlaintext, parsed.timestamp, input.rawBody);
|
|
65
|
+
const a = Buffer.from(parsed.v1, 'hex');
|
|
66
|
+
const b = Buffer.from(expected, 'hex');
|
|
67
|
+
if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
|
|
68
|
+
throw new ToolCallbackSignatureError('mismatch', 'Signature does not match');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseSignatureHeader(raw) {
|
|
72
|
+
if (!raw)
|
|
73
|
+
return null;
|
|
74
|
+
let timestamp;
|
|
75
|
+
let v1;
|
|
76
|
+
for (const part of raw.split(',').map((p) => p.trim())) {
|
|
77
|
+
if (part.startsWith('t='))
|
|
78
|
+
timestamp = Number(part.slice(2));
|
|
79
|
+
else if (part.startsWith('v1='))
|
|
80
|
+
v1 = part.slice(3);
|
|
81
|
+
}
|
|
82
|
+
if (!timestamp || !v1 || Number.isNaN(timestamp))
|
|
83
|
+
return null;
|
|
84
|
+
return { timestamp, v1 };
|
|
85
|
+
}
|
|
86
|
+
function computeHmac(secretPlaintext, timestamp, rawBody) {
|
|
87
|
+
return (0, node_crypto_1.createHmac)('sha256', secretPlaintext)
|
|
88
|
+
.update(`${timestamp}.${rawBody}`)
|
|
89
|
+
.digest('hex');
|
|
90
|
+
}
|