@segmentstream/agent 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +178 -2
- package/dist/extension/_manifest.json +13 -0
- package/dist/extension/channels/telegram.d.ts +6 -0
- package/dist/extension/channels/telegram.mjs +15 -0
- package/dist/extension/config.d.ts +99 -0
- package/dist/extension/config.mjs +144 -0
- package/dist/extension/connections/ontology.d.ts +7 -0
- package/dist/extension/connections/ontology.mjs +11 -0
- package/dist/extension/extension.d.ts +19 -0
- package/dist/extension/extension.mjs +15 -0
- package/dist/extension/instructions.md +13 -0
- package/dist/extension/lib/connection-proxy.d.ts +57 -0
- package/dist/extension/lib/connection-proxy.mjs +113 -0
- package/dist/extension/lib/environment.d.ts +30 -0
- package/dist/extension/lib/environment.mjs +46 -0
- package/dist/extension/lib/identities.d.ts +65 -0
- package/dist/extension/lib/identities.mjs +133 -0
- package/dist/extension/lib/ontology-connection.d.ts +36 -0
- package/dist/extension/lib/ontology-connection.mjs +48 -0
- package/dist/extension/lib/runtime-token.d.ts +17 -0
- package/dist/extension/lib/runtime-token.mjs +31 -0
- package/dist/extension/lib/telegram/adapter.d.ts +29 -0
- package/dist/extension/lib/telegram/adapter.mjs +115 -0
- package/dist/extension/lib/telegram/bot.d.ts +4 -0
- package/dist/extension/lib/telegram/bot.mjs +45 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +22 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +37 -0
- package/dist/extension/lib/telegram/inbound.d.ts +35 -0
- package/dist/extension/lib/telegram/inbound.mjs +117 -0
- package/dist/extension/lib/telegram/observed.d.ts +32 -0
- package/dist/extension/lib/telegram/observed.mjs +47 -0
- package/dist/extension/lib/telegram/principal.d.ts +20 -0
- package/dist/extension/lib/telegram/principal.mjs +40 -0
- package/dist/extension/lib/telegram/proxy-fetch.d.ts +28 -0
- package/dist/extension/lib/telegram/proxy-fetch.mjs +69 -0
- package/dist/extension/lib/telegram/router.d.ts +69 -0
- package/dist/extension/lib/telegram/router.mjs +64 -0
- package/dist/extension/lib/telegram/silent.d.ts +9 -0
- package/dist/extension/lib/telegram/silent.mjs +15 -0
- package/dist/extension/lib/telegram/voice.d.ts +52 -0
- package/dist/extension/lib/telegram/voice.mjs +115 -0
- package/dist/extension/lib/telegram/webhook.d.ts +13 -0
- package/dist/extension/lib/telegram/webhook.mjs +23 -0
- package/dist/extension/model.d.ts +29 -0
- package/dist/extension/model.mjs +23 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +8 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.mjs +7 -0
- package/package.json +76 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `segmentstream__ontology`: the ontology's MCP tools, acting as the member of each turn. It is
|
|
3
|
+
* resolved per turn because a group chat's session serves several members; a turn without a
|
|
4
|
+
* paired member, or a mount with `ontology.connection: false`, gets no connection.
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: import("eve").DynamicSentinel<import("eve/connections").DynamicConnectionResult>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import segmentstream from "../extension.mjs";
|
|
8
|
+
import { ontologyConnection } from "../lib/ontology-connection.mjs";
|
|
9
|
+
import { defineDynamic } from "eve/connections";
|
|
10
|
+
var ontology_default = defineDynamic({ events: { "turn.started": (_event, context) => segmentstream.config.ontology.connection ? ontologyConnection({ principal: context.session.auth.current }) : null } });
|
|
11
|
+
export { ontology_default as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { manifestOf } from "./config.js";
|
|
2
|
+
import { model } from "./model.js";
|
|
3
|
+
export declare const segmentstream: import("eve/extension").ExtensionHandle<{
|
|
4
|
+
readonly "~standard": {
|
|
5
|
+
readonly version: 1;
|
|
6
|
+
readonly vendor: "segmentstream";
|
|
7
|
+
readonly validate: typeof import("./config.js").validateConfig;
|
|
8
|
+
readonly types: {
|
|
9
|
+
readonly input: import("./config.js").SegmentStreamAgentConfigInput;
|
|
10
|
+
readonly output: import("./config.js").SegmentStreamAgentConfig;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}> & {
|
|
14
|
+
model: typeof model;
|
|
15
|
+
manifestOf: typeof manifestOf;
|
|
16
|
+
};
|
|
17
|
+
export default segmentstream;
|
|
18
|
+
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, manifestOf, type SegmentStreamAgentConfig, type SegmentStreamAgentConfigInput, type TelegramPolicy, type TelegramSettings, } from "./config.js";
|
|
19
|
+
export { chooseModel, model, type DynamicModel, type ModelChoice } from "./model.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, configSchema, manifestOf } from "./config.mjs";
|
|
8
|
+
import { chooseModel, model } from "./model.mjs";
|
|
9
|
+
import { defineExtension } from "eve/extension";
|
|
10
|
+
const extension = defineExtension({ config: configSchema });
|
|
11
|
+
const segmentstream = Object.assign(extension, {
|
|
12
|
+
model,
|
|
13
|
+
manifestOf
|
|
14
|
+
});
|
|
15
|
+
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, chooseModel, segmentstream as default, segmentstream, manifestOf, model };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SegmentStream platform
|
|
2
|
+
|
|
3
|
+
You are the workspace's agent on SegmentStream. Every conversation runs as the member who is
|
|
4
|
+
talking to you, and you act with exactly their permissions.
|
|
5
|
+
|
|
6
|
+
- The company's records live in the ontology, reached through the `segmentstream__ontology`
|
|
7
|
+
connection. Call `describe` for the catalog of entities and actions, and `me` for whom you act
|
|
8
|
+
for and what they may do. The tools you see are already narrowed to that member's permissions;
|
|
9
|
+
if a tool is missing, the member may not use it, so say so instead of looking for a way around.
|
|
10
|
+
- Every change goes through an action. Never claim a record changed unless an action reported it.
|
|
11
|
+
- Never invent records or values. Say when the ontology does not hold what was asked.
|
|
12
|
+
- When you have been routed a message but a reply would add nothing, answer with exactly
|
|
13
|
+
`[SILENT]` and nothing else. Use it only in that case; it suppresses delivery to the chat.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ProxyEnvironment } from "./environment.js";
|
|
2
|
+
import type { RuntimeToken } from "./runtime-token.js";
|
|
3
|
+
/**
|
|
4
|
+
* A client for backend-next's connection proxy, the route the deployed ontology service already
|
|
5
|
+
* uses (`ontology-connections-spec.md`, "The proxy for the ontology service"). The agent is the
|
|
6
|
+
* same deployment, so it authenticates the same way: the Vercel OIDC token proves the workspace's
|
|
7
|
+
* application, and the organization id only selects the tenant. The provider credential stays in
|
|
8
|
+
* backend-next; this process never sees it.
|
|
9
|
+
*/
|
|
10
|
+
/** The largest request or response body the proxy forwards. */
|
|
11
|
+
export declare const MAXIMUM_PROXY_BODY_BYTES: number;
|
|
12
|
+
/** One provider request: `OntologyProxyRequest` before it is encoded. */
|
|
13
|
+
export interface ProxyCall {
|
|
14
|
+
/** One of the connection's APIs, such as Telegram's `bot` or `file`; its default when omitted. */
|
|
15
|
+
readonly api?: string;
|
|
16
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
17
|
+
/** The path and query relative to the API's base URL. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly headers: ReadonlyArray<readonly [string, string]>;
|
|
20
|
+
readonly body: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
/** What the provider answered, buffered by the proxy. */
|
|
23
|
+
export interface ProxyReply {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly headers: ReadonlyArray<readonly [string, string]>;
|
|
26
|
+
readonly body: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SegmentStream refused or could not complete a proxy call. A provider's non-2xx status is a
|
|
30
|
+
* reply, not this error. `code` is the proxy's error code, such as `connection_not_granted`.
|
|
31
|
+
*/
|
|
32
|
+
export declare class ConnectionProxyError extends Error {
|
|
33
|
+
readonly name = "ConnectionProxyError";
|
|
34
|
+
readonly connection: string;
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly status: number | undefined;
|
|
37
|
+
readonly requestId: string | undefined;
|
|
38
|
+
constructor(input: {
|
|
39
|
+
readonly connection: string;
|
|
40
|
+
readonly code: string;
|
|
41
|
+
readonly message: string;
|
|
42
|
+
readonly status?: number;
|
|
43
|
+
readonly requestId?: string;
|
|
44
|
+
readonly cause?: unknown;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export interface ConnectionProxy {
|
|
48
|
+
readonly connection: string;
|
|
49
|
+
request(call: ProxyCall): Promise<ProxyReply>;
|
|
50
|
+
}
|
|
51
|
+
/** Sends calls through one connection with the deployment's OIDC token. */
|
|
52
|
+
export declare function connectionProxy(input: {
|
|
53
|
+
readonly environment: ProxyEnvironment;
|
|
54
|
+
readonly connection: string;
|
|
55
|
+
readonly token: RuntimeToken;
|
|
56
|
+
readonly fetch?: typeof fetch;
|
|
57
|
+
}): ConnectionProxy;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
const MAXIMUM_PROXY_BODY_BYTES = 2097152;
|
|
8
|
+
const CALL_TIMEOUT_MS = 4e4;
|
|
9
|
+
var ConnectionProxyError = class extends Error {
|
|
10
|
+
name = "ConnectionProxyError";
|
|
11
|
+
connection;
|
|
12
|
+
code;
|
|
13
|
+
status;
|
|
14
|
+
requestId;
|
|
15
|
+
constructor(input) {
|
|
16
|
+
super(input.message, input.cause === void 0 ? void 0 : { cause: input.cause });
|
|
17
|
+
this.connection = input.connection;
|
|
18
|
+
this.code = input.code;
|
|
19
|
+
this.status = input.status;
|
|
20
|
+
this.requestId = input.requestId;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isHeaderPairs(value) {
|
|
27
|
+
return Array.isArray(value) && value.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string");
|
|
28
|
+
}
|
|
29
|
+
function decodeReply(connection, value) {
|
|
30
|
+
if (!isRecord(value) || typeof value.statusCode !== "number" || !isHeaderPairs(value.headers) || typeof value.body !== "string") throw new ConnectionProxyError({
|
|
31
|
+
connection,
|
|
32
|
+
code: "invalid_response",
|
|
33
|
+
message: "The connection proxy answered with an invalid response."
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
status: value.statusCode,
|
|
37
|
+
headers: value.headers,
|
|
38
|
+
body: new Uint8Array(Buffer.from(value.body, "base64"))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function refusal(connection, status, value, cause) {
|
|
42
|
+
const code = isRecord(value) && typeof value.error === "string" ? value.error : `http_${status}`;
|
|
43
|
+
const detail = isRecord(value) && typeof value.detail === "string" ? value.detail : void 0;
|
|
44
|
+
const requestId = isRecord(value) && typeof value.requestId === "string" ? value.requestId : void 0;
|
|
45
|
+
return new ConnectionProxyError({
|
|
46
|
+
connection,
|
|
47
|
+
code,
|
|
48
|
+
status,
|
|
49
|
+
message: detail ?? `SegmentStream refused the call through the connection ${connection} (${code}).`,
|
|
50
|
+
...requestId === void 0 ? {} : { requestId },
|
|
51
|
+
...cause === void 0 ? {} : { cause }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function connectionProxy(input) {
|
|
55
|
+
const { environment, connection } = input;
|
|
56
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
57
|
+
const url = `${`${environment.segmentstreamUrl}/api/internal/v1/workspaces/${environment.workspaceId}`}/ontology/connections/${encodeURIComponent(connection)}/request`;
|
|
58
|
+
return {
|
|
59
|
+
connection,
|
|
60
|
+
async request(call) {
|
|
61
|
+
if (call.body.byteLength > 2097152) throw new ConnectionProxyError({
|
|
62
|
+
connection,
|
|
63
|
+
code: "invalid_request",
|
|
64
|
+
message: "The request body exceeds the proxy's 2 MiB limit."
|
|
65
|
+
});
|
|
66
|
+
const token = await input.token();
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetchImpl(url, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"x-segmentstream-runtime-identity": token,
|
|
73
|
+
"x-segmentstream-organization-id": environment.organizationId,
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
accept: "application/json"
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
...call.api === void 0 ? {} : { api: call.api },
|
|
79
|
+
method: call.method,
|
|
80
|
+
path: call.path,
|
|
81
|
+
headers: call.headers,
|
|
82
|
+
body: Buffer.from(call.body).toString("base64")
|
|
83
|
+
}),
|
|
84
|
+
redirect: "error",
|
|
85
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new ConnectionProxyError({
|
|
89
|
+
connection,
|
|
90
|
+
code: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "unavailable",
|
|
91
|
+
message: "SegmentStream's connection proxy could not be reached.",
|
|
92
|
+
cause: error
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = JSON.parse(text);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (response.ok) throw new ConnectionProxyError({
|
|
101
|
+
connection,
|
|
102
|
+
code: "invalid_response",
|
|
103
|
+
message: "The connection proxy answered with an invalid response.",
|
|
104
|
+
cause: error
|
|
105
|
+
});
|
|
106
|
+
throw refusal(connection, response.status, void 0, error);
|
|
107
|
+
}
|
|
108
|
+
if (!response.ok) throw refusal(connection, response.status, value);
|
|
109
|
+
return decodeReply(connection, value);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export { ConnectionProxyError, MAXIMUM_PROXY_BODY_BYTES, connectionProxy };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment's environment, as the platform sets it on the workspace's Vercel project. Only
|
|
3
|
+
* the webhook secret is sensitive; the rest names where things are.
|
|
4
|
+
*/
|
|
5
|
+
export type Environment = Readonly<Record<string, string | undefined>>;
|
|
6
|
+
/** A variable the agent needs is missing or malformed. The message names it, never its value. */
|
|
7
|
+
export declare class AgentEnvironmentError extends Error {
|
|
8
|
+
readonly name = "AgentEnvironmentError";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Where the workspace app answers: the ontology service's `/mcp` and `/api/ontology/*`, and the
|
|
12
|
+
* app's `/agent/connect` page. `SEGMENTSTREAM_AGENT_ONTOLOGY_URL` overrides it in local
|
|
13
|
+
* development; deployments use their production domain.
|
|
14
|
+
*/
|
|
15
|
+
export declare function appOrigin(environment?: Environment): string;
|
|
16
|
+
/** What the connection proxy needs: backend-next's URL and the tenant it selects. */
|
|
17
|
+
export interface ProxyEnvironment {
|
|
18
|
+
readonly segmentstreamUrl: string;
|
|
19
|
+
readonly workspaceId: string;
|
|
20
|
+
readonly organizationId: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function proxyEnvironment(environment?: Environment): ProxyEnvironment;
|
|
23
|
+
/** The secret Telegram echoes in `X-Telegram-Bot-Api-Secret-Token`; `undefined` when unset. */
|
|
24
|
+
export declare function webhookSecret(environment?: Environment): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Channels answer only in production. A preview shares the production bot's credential through
|
|
27
|
+
* the same connection, so it must never answer a real chat.
|
|
28
|
+
*/
|
|
29
|
+
export declare function channelsEnabled(environment?: Environment): boolean;
|
|
30
|
+
export declare function isPreview(environment?: Environment): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
var AgentEnvironmentError = class extends Error {
|
|
8
|
+
name = "AgentEnvironmentError";
|
|
9
|
+
};
|
|
10
|
+
function required(environment, name) {
|
|
11
|
+
const value = environment[name];
|
|
12
|
+
if (value === void 0 || value.trim() === "") throw new AgentEnvironmentError(`${name} is not set on this deployment.`);
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
function withoutTrailingSlashes(url) {
|
|
16
|
+
let end = url.length;
|
|
17
|
+
while (end > 0 && url[end - 1] === "/") end -= 1;
|
|
18
|
+
return url.slice(0, end);
|
|
19
|
+
}
|
|
20
|
+
function appOrigin(environment = process.env) {
|
|
21
|
+
const override = environment.SEGMENTSTREAM_AGENT_ONTOLOGY_URL;
|
|
22
|
+
if (override !== void 0 && override.trim() !== "") {
|
|
23
|
+
const url = URL.canParse(override) ? new URL(override) : void 0;
|
|
24
|
+
if (url === void 0 || url.protocol !== "https:" && url.protocol !== "http:") throw new AgentEnvironmentError("SEGMENTSTREAM_AGENT_ONTOLOGY_URL must be an http(s) URL.");
|
|
25
|
+
return withoutTrailingSlashes(`${url.origin}${url.pathname}`);
|
|
26
|
+
}
|
|
27
|
+
return `https://${required(environment, "VERCEL_PROJECT_PRODUCTION_URL")}`;
|
|
28
|
+
}
|
|
29
|
+
function proxyEnvironment(environment = process.env) {
|
|
30
|
+
return {
|
|
31
|
+
segmentstreamUrl: withoutTrailingSlashes(required(environment, "SEGMENTSTREAM_URL")),
|
|
32
|
+
workspaceId: required(environment, "SEGMENTSTREAM_WORKSPACE_ID"),
|
|
33
|
+
organizationId: required(environment, "SEGMENTSTREAM_ORGANIZATION_ID")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function webhookSecret(environment = process.env) {
|
|
37
|
+
const secret = environment.SEGMENTSTREAM_AGENT_WEBHOOK_SECRET;
|
|
38
|
+
return secret === void 0 || secret === "" ? void 0 : secret;
|
|
39
|
+
}
|
|
40
|
+
function channelsEnabled(environment = process.env) {
|
|
41
|
+
return environment.VERCEL_ENV === "production";
|
|
42
|
+
}
|
|
43
|
+
function isPreview(environment = process.env) {
|
|
44
|
+
return environment.VERCEL_ENV === "preview";
|
|
45
|
+
}
|
|
46
|
+
export { AgentEnvironmentError, appOrigin, channelsEnabled, isPreview, proxyEnvironment, webhookSecret };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RuntimeToken } from "./runtime-token.js";
|
|
2
|
+
/**
|
|
3
|
+
* The ontology service's pairing routes (`agent-design.md`, section 13, "Identity"). The agent
|
|
4
|
+
* calls them as the deployment, with its OIDC token: it looks up which member a chat account is
|
|
5
|
+
* paired with, and asks for a pairing code when it is paired with nobody. The member approves the
|
|
6
|
+
* code on the app's `/agent/connect` page.
|
|
7
|
+
*/
|
|
8
|
+
/** A member as the ontology resolves a paired chat account, with their current roles. */
|
|
9
|
+
export interface PairedMember {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly name: string | null;
|
|
12
|
+
readonly roles: ReadonlyArray<string>;
|
|
13
|
+
}
|
|
14
|
+
export type IdentityLookup = {
|
|
15
|
+
readonly status: "paired";
|
|
16
|
+
readonly member: PairedMember;
|
|
17
|
+
}
|
|
18
|
+
/** The account is paired with nobody. */
|
|
19
|
+
| {
|
|
20
|
+
readonly status: "unknown";
|
|
21
|
+
}
|
|
22
|
+
/** The pairing was revoked, or the member is inactive. */
|
|
23
|
+
| {
|
|
24
|
+
readonly status: "refused";
|
|
25
|
+
};
|
|
26
|
+
/** A pairing code the member approves at `/agent/connect?code=<code>`. */
|
|
27
|
+
export interface Pairing {
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly expiresAt: string;
|
|
30
|
+
}
|
|
31
|
+
/** A chat account on one channel, such as Telegram user `42`. */
|
|
32
|
+
export interface ChatAccount {
|
|
33
|
+
readonly provider: string;
|
|
34
|
+
readonly externalId: string;
|
|
35
|
+
}
|
|
36
|
+
export interface AgentIdentities {
|
|
37
|
+
lookup(account: ChatAccount): Promise<IdentityLookup>;
|
|
38
|
+
/** Issues a pairing code, or returns the account's pending one while it has time left. */
|
|
39
|
+
createPairing(account: ChatAccount & {
|
|
40
|
+
readonly display?: string;
|
|
41
|
+
}): Promise<Pairing>;
|
|
42
|
+
}
|
|
43
|
+
/** The ontology service refused or failed a pairing call. Carries its error code, not its body. */
|
|
44
|
+
export declare class AgentIdentityError extends Error {
|
|
45
|
+
readonly name = "AgentIdentityError";
|
|
46
|
+
readonly code: string;
|
|
47
|
+
readonly status: number | undefined;
|
|
48
|
+
constructor(input: {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
readonly message: string;
|
|
51
|
+
readonly status?: number;
|
|
52
|
+
readonly cause?: unknown;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The pairing client. Every message looks its sender up afresh, so a revoked account or a removed
|
|
57
|
+
* member is refused at once; only pending pairing codes are remembered per process.
|
|
58
|
+
*/
|
|
59
|
+
export declare function agentIdentities(input: {
|
|
60
|
+
/** The workspace app's origin, where the ontology service answers `/api/ontology/*`. */
|
|
61
|
+
readonly origin: string;
|
|
62
|
+
readonly token: RuntimeToken;
|
|
63
|
+
readonly fetch?: typeof fetch;
|
|
64
|
+
readonly now?: () => number;
|
|
65
|
+
}): AgentIdentities;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
var AgentIdentityError = class extends Error {
|
|
8
|
+
name = "AgentIdentityError";
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
constructor(input) {
|
|
12
|
+
super(input.message, input.cause === void 0 ? void 0 : { cause: input.cause });
|
|
13
|
+
this.code = input.code;
|
|
14
|
+
this.status = input.status;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const PAIRING_MARGIN_MS = 6e4;
|
|
18
|
+
const MAXIMUM_CACHED_ACCOUNTS = 1e3;
|
|
19
|
+
const CALL_TIMEOUT_MS = 1e4;
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function accountKey(account) {
|
|
24
|
+
return `${account.provider}:${account.externalId}`;
|
|
25
|
+
}
|
|
26
|
+
function remember(cache, key, value) {
|
|
27
|
+
cache.delete(key);
|
|
28
|
+
cache.set(key, value);
|
|
29
|
+
if (cache.size > MAXIMUM_CACHED_ACCOUNTS) {
|
|
30
|
+
const oldest = cache.keys().next();
|
|
31
|
+
if (oldest.done !== true) cache.delete(oldest.value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function pairedMember(value) {
|
|
35
|
+
const user = isRecord(value) ? value.user : void 0;
|
|
36
|
+
const roles = isRecord(value) ? value.roles : void 0;
|
|
37
|
+
if (!isRecord(user) || typeof user.id !== "string" || user.name !== void 0 && user.name !== null && typeof user.name !== "string" || !Array.isArray(roles) || !roles.every((role) => typeof role === "string")) throw new AgentIdentityError({
|
|
38
|
+
code: "invalid_response",
|
|
39
|
+
message: "The ontology service answered the identity lookup with an invalid body."
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
id: user.id,
|
|
43
|
+
name: typeof user.name === "string" ? user.name : null,
|
|
44
|
+
roles
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function pairing(value) {
|
|
48
|
+
if (!isRecord(value) || typeof value.code !== "string" || typeof value.expires_at !== "string") throw new AgentIdentityError({
|
|
49
|
+
code: "invalid_response",
|
|
50
|
+
message: "The ontology service answered the pairing request with an invalid body."
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
code: value.code,
|
|
54
|
+
expiresAt: value.expires_at
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function refusal(response, call) {
|
|
58
|
+
const status = response.status;
|
|
59
|
+
let value;
|
|
60
|
+
try {
|
|
61
|
+
value = await response.json();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return new AgentIdentityError({
|
|
64
|
+
code: `http_${status}`,
|
|
65
|
+
status,
|
|
66
|
+
message: `The ontology service refused the ${call} with HTTP ${status} and a body that is not JSON.`,
|
|
67
|
+
cause: error
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const body = isRecord(value) ? value : {};
|
|
71
|
+
const code = typeof body.error === "string" ? body.error : `http_${status}`;
|
|
72
|
+
return new AgentIdentityError({
|
|
73
|
+
code,
|
|
74
|
+
status,
|
|
75
|
+
message: `The ontology service refused the ${call} with HTTP ${status}, ${code}${typeof body.detail === "string" ? `: ${body.detail}` : ""}${typeof body.request_id === "string" ? ` (request ${body.request_id})` : ""}.`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function agentIdentities(input) {
|
|
79
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
80
|
+
const now = input.now ?? Date.now;
|
|
81
|
+
const pending = new Map();
|
|
82
|
+
async function call(path, request) {
|
|
83
|
+
const token = await input.token();
|
|
84
|
+
try {
|
|
85
|
+
return await fetchImpl(`${input.origin}/api/ontology/agent${path}`, {
|
|
86
|
+
method: request.body === void 0 ? "GET" : "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
authorization: `Bearer ${token}`,
|
|
89
|
+
accept: "application/json",
|
|
90
|
+
...request.body === void 0 ? {} : { "content-type": "application/json" }
|
|
91
|
+
},
|
|
92
|
+
...request.body === void 0 ? {} : { body: JSON.stringify(request.body) },
|
|
93
|
+
redirect: "error",
|
|
94
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new AgentIdentityError({
|
|
98
|
+
code: "unavailable",
|
|
99
|
+
message: "The ontology service could not be reached.",
|
|
100
|
+
cause: error
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
async lookup(account) {
|
|
106
|
+
const response = await call(`/identities/${encodeURIComponent(account.provider)}/${encodeURIComponent(account.externalId)}`, {});
|
|
107
|
+
if (response.status === 404) return { status: "unknown" };
|
|
108
|
+
if (response.status === 403) return { status: "refused" };
|
|
109
|
+
if (!response.ok) throw await refusal(response, "identity lookup");
|
|
110
|
+
const member = pairedMember(await response.json());
|
|
111
|
+
pending.delete(accountKey(account));
|
|
112
|
+
return {
|
|
113
|
+
status: "paired",
|
|
114
|
+
member
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
async createPairing(account) {
|
|
118
|
+
const key = accountKey(account);
|
|
119
|
+
const existing = pending.get(key);
|
|
120
|
+
if (existing !== void 0 && Date.parse(existing.expiresAt) - PAIRING_MARGIN_MS > now()) return existing;
|
|
121
|
+
const response = await call("/pairings", { body: {
|
|
122
|
+
provider: account.provider,
|
|
123
|
+
external_id: account.externalId,
|
|
124
|
+
...account.display === void 0 ? {} : { display: account.display }
|
|
125
|
+
} });
|
|
126
|
+
if (!response.ok) throw await refusal(response, "pairing request");
|
|
127
|
+
const created = pairing(await response.json());
|
|
128
|
+
remember(pending, key, created);
|
|
129
|
+
return created;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export { AgentIdentityError, agentIdentities };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
|
+
import { type McpClientConnectionDefinition } from "eve/connections";
|
|
3
|
+
import { type Environment } from "./environment.js";
|
|
4
|
+
import { type RuntimeToken } from "./runtime-token.js";
|
|
5
|
+
/**
|
|
6
|
+
* The ontology as tools: an MCP connection to the workspace app's `/mcp`
|
|
7
|
+
* (`ontology-mcp-design.md`). The agent authenticates as the deployment with its Vercel OIDC token
|
|
8
|
+
* and names the member it acts for in `X-SegmentStream-Act-As`; the ontology service checks that
|
|
9
|
+
* the member is active and has a live pairing, lists only the tools the member may run, and
|
|
10
|
+
* records the member in history.
|
|
11
|
+
*/
|
|
12
|
+
/** The header that names the member a runtime caller acts for. */
|
|
13
|
+
export declare const ACT_AS_HEADER = "X-SegmentStream-Act-As";
|
|
14
|
+
/** The session has no member to act for, so the ontology must not be reached. */
|
|
15
|
+
export declare class MemberRequiredError extends Error {
|
|
16
|
+
readonly name = "MemberRequiredError";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The headers that name the member, from the session principal of the turn. The connection fails
|
|
20
|
+
* without one, and fails when the turn's member is not the one the connection instance was opened
|
|
21
|
+
* for, so a client opened for one member never serves another.
|
|
22
|
+
*/
|
|
23
|
+
export declare function actAsHeaders(input: {
|
|
24
|
+
readonly principal: SessionAuthContext | null;
|
|
25
|
+
readonly expectedMemberId: string;
|
|
26
|
+
}): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* The connection for one turn, or `null` when the turn has no member principal: an unpaired
|
|
29
|
+
* caller never reaches the ontology. The instance key names the member, so eve opens a separate
|
|
30
|
+
* client per member even when members share a group chat's session.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ontologyConnection(input: {
|
|
33
|
+
readonly principal: SessionAuthContext | null;
|
|
34
|
+
readonly environment?: Environment;
|
|
35
|
+
readonly token?: RuntimeToken;
|
|
36
|
+
}): McpClientConnectionDefinition | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { appOrigin } from "./environment.mjs";
|
|
8
|
+
import { memberIdOf } from "./telegram/principal.mjs";
|
|
9
|
+
import { runtimeToken, tokenExpiry } from "./runtime-token.mjs";
|
|
10
|
+
import { defineMcpClientConnection } from "eve/connections";
|
|
11
|
+
const ACT_AS_HEADER = "X-SegmentStream-Act-As";
|
|
12
|
+
const DESCRIPTION = "The company's ontology: its records, their history, and the actions that change them, narrowed to what the member you act for may read and do. Call describe for the catalog and me for the member.";
|
|
13
|
+
var MemberRequiredError = class extends Error {
|
|
14
|
+
name = "MemberRequiredError";
|
|
15
|
+
};
|
|
16
|
+
function actAsHeaders(input) {
|
|
17
|
+
const memberId = memberIdOf(input.principal);
|
|
18
|
+
if (memberId === void 0) throw new MemberRequiredError("The ontology is reachable only for a member paired with this agent.");
|
|
19
|
+
if (memberId !== input.expectedMemberId) throw new MemberRequiredError("The ontology connection was opened for another member.");
|
|
20
|
+
return { [ACT_AS_HEADER]: memberId };
|
|
21
|
+
}
|
|
22
|
+
function ontologyConnection(input) {
|
|
23
|
+
const memberId = memberIdOf(input.principal);
|
|
24
|
+
if (memberId === void 0) return null;
|
|
25
|
+
const origin = appOrigin(input.environment ?? process.env);
|
|
26
|
+
const token = input.token ?? runtimeToken;
|
|
27
|
+
return defineMcpClientConnection({
|
|
28
|
+
url: `${origin}/mcp`,
|
|
29
|
+
description: DESCRIPTION,
|
|
30
|
+
instanceKey: `${origin}/mcp#member:${memberId}`,
|
|
31
|
+
auth: {
|
|
32
|
+
credentialOwner: "app",
|
|
33
|
+
getToken: async () => {
|
|
34
|
+
const value = await token();
|
|
35
|
+
const expiresAt = tokenExpiry(value);
|
|
36
|
+
return expiresAt === void 0 ? { token: value } : {
|
|
37
|
+
token: value,
|
|
38
|
+
expiresAt
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
headers: (context) => actAsHeaders({
|
|
43
|
+
principal: context.session.auth.current,
|
|
44
|
+
expectedMemberId: memberId
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export { ACT_AS_HEADER, MemberRequiredError, actAsHeaders, ontologyConnection };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment's Vercel OIDC token. It proves the workspace's agent to the ontology service and
|
|
3
|
+
* to backend-next's connection proxy. Vercel sets it on each function request
|
|
4
|
+
* (`x-vercel-oidc-token`), and `vercel env pull` writes `VERCEL_OIDC_TOKEN` for local development;
|
|
5
|
+
* `@vercel/oidc` reads both. It is used only in runtime code and never reaches the model.
|
|
6
|
+
*/
|
|
7
|
+
export type RuntimeToken = () => Promise<string>;
|
|
8
|
+
/** The deployment has no OIDC token: it does not run on Vercel, or OIDC is off for the project. */
|
|
9
|
+
export declare class RuntimeTokenError extends Error {
|
|
10
|
+
readonly name = "RuntimeTokenError";
|
|
11
|
+
}
|
|
12
|
+
export declare const runtimeToken: RuntimeToken;
|
|
13
|
+
/**
|
|
14
|
+
* When the token expires, in milliseconds since the epoch, read from its `exp` claim without
|
|
15
|
+
* verifying it: the token is our own, and the value only schedules a refresh.
|
|
16
|
+
*/
|
|
17
|
+
export declare function tokenExpiry(token: string): number | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
8
|
+
var RuntimeTokenError = class extends Error {
|
|
9
|
+
name = "RuntimeTokenError";
|
|
10
|
+
};
|
|
11
|
+
const runtimeToken = async () => {
|
|
12
|
+
let token;
|
|
13
|
+
try {
|
|
14
|
+
token = await getVercelOidcToken();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new RuntimeTokenError("The deployment has no Vercel OIDC token. The agent reaches SegmentStream only from a Vercel deployment.", { cause: error });
|
|
17
|
+
}
|
|
18
|
+
if (token === "") throw new RuntimeTokenError("The deployment's Vercel OIDC token is empty.");
|
|
19
|
+
return token;
|
|
20
|
+
};
|
|
21
|
+
function tokenExpiry(token) {
|
|
22
|
+
const payload = token.split(".")[1];
|
|
23
|
+
if (payload === void 0) return;
|
|
24
|
+
try {
|
|
25
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
26
|
+
if (typeof claims === "object" && claims !== null && "exp" in claims && typeof claims.exp === "number") return claims.exp * 1e3;
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export { RuntimeTokenError, runtimeToken, tokenExpiry };
|