@intelli-1113/stratos-sdk 1.3.5 → 1.3.6

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.
@@ -1,184 +1,182 @@
1
- // Network-level budget / lifecycle enforcement.
2
- //
3
- // WHY THE NETWORK LAYER, NOT THE SDK LAYER
4
- // -----------------------------------------------------------------------------
5
- // OpenAI, Anthropic, LangChain, Vercel AI, Google GenAI — every one of them
6
- // ultimately does the same thing: send an outbound HTTP request to the
7
- // provider's API. Traceloop's own instrumentation (which this SDK depends on
8
- // but does not own — @traceloop/instrumentation-openai et al.) exposes no
9
- // pre-call hook that can refuse a request; its config surface is
10
- // traceContent / enrichTokens / exceptionLogger / uploadBase64Image only.
11
- // Wrapping every provider SDK's call function individually is also a moving
12
- // target — a "custom agent" built directly on a raw SDK gets no protection
13
- // until someone ships a wrapper for that specific library.
14
- //
15
- // Patching global `fetch` (and `http`/`https` as a fallback for older
16
- // transports) catches all of them in ONE place, before the request leaves the
17
- // process, with no per-provider code and no dependency on Traceloop's
18
- // internals. This module is installed from register.js, before any provider
19
- // SDK is imported by the agent's own code — the same ordering guarantee the
20
- // Vercel AI ESM loader already relies on.
21
- //
22
- // This does NOT cover the MCP proxy path (src/mcp-proxy.js): that spawns a
23
- // separate child process and proxies its stdio, so patching fetch in THIS
24
- // process has no effect on the child's own network calls. The proxy has its
25
- // own, necessary check at the JSON-RPC `tools/call` boundary instead.
26
-
27
- import http from "node:http";
28
- import https from "node:https";
29
-
30
- export class StratosBudgetBlockedError extends Error {
31
- constructor(reason) {
32
- super(`[stratos] request blocked: ${reason || "budget or lifecycle limit reached"}`);
33
- this.name = "StratosBudgetBlockedError";
34
- this.stratosBlocked = true;
35
- }
36
- }
37
-
38
- // Cached from the heartbeat response (see index.js / mcp-proxy.js). Starts
39
- // unblocked: an agent must never be frozen just because it hasn't heard back
40
- // from Stratos yet — same fail-open philosophy as the server's own enforcement
41
- // check (lib/quotaEnforce.ts: "fail open, loudly").
42
- let _blocked = false;
43
- let _reason = null;
44
-
45
- export function setBlocked(blocked, reason) {
46
- _blocked = !!blocked;
47
- _reason = reason || null;
48
- }
49
-
50
- export function isBlocked() {
51
- return { blocked: _blocked, reason: _reason };
52
- }
53
-
54
- // Hostnames of LLM/agent providers to guard. Extend without an SDK release via
55
- // STRATOS_BLOCK_HOSTS (comma-separated substrings) — e.g. a private Azure
56
- // OpenAI deployment hostname, or a self-hosted gateway.
57
- const DEFAULT_PROVIDER_HOSTS = [
58
- "api.openai.com",
59
- "api.anthropic.com",
60
- "generativelanguage.googleapis.com",
61
- "aiplatform.googleapis.com",
62
- "bedrock-runtime.", // *.amazonaws.com region-scoped, matched by substring
63
- "api.cohere.ai",
64
- "api.mistral.ai",
65
- "openrouter.ai",
66
- "api.together.xyz",
67
- "api.groq.com",
68
- "openai.azure.com",
69
- ];
70
-
71
- function providerHosts() {
72
- const extra = (process.env.STRATOS_BLOCK_HOSTS || "")
73
- .split(",")
74
- .map((s) => s.trim())
75
- .filter(Boolean);
76
- // P1-02: Add "api." catch-all to block future/unlisted providers (NVIDIA, DeepSeek, Fireworks, xAI, etc.)
77
- // while keeping explicit list for clarity
78
- return [...DEFAULT_PROVIDER_HOSTS, ...extra, "api."];
79
- }
80
-
81
- // Never guard Stratos's own host: the agent has to be able to reach Stratos to
82
- // find out it has been unblocked. If this returned the wrong host the agent
83
- // would deadlock itself — checked defensively, not just assumed.
84
- function ownHost() {
85
- try {
86
- return new URL(process.env.STRATOS_URL || "http://localhost:4000").host;
87
- } catch {
88
- return "";
89
- }
90
- }
91
-
92
- function hostFromInput(input) {
93
- try {
94
- if (typeof input === "string") return new URL(input).host;
95
- if (input instanceof URL) return input.host;
96
- if (input && typeof input === "object") {
97
- if (input.url) return new URL(input.url).host; // Request-like
98
- if (input.hostname || input.host) return String(input.hostname || input.host);
99
- }
100
- } catch {
101
- // Not a parseable URL. Let it through — this guard only ever restricts
102
- // requests to a known provider host, never anything it can't identify.
103
- }
104
- return "";
105
- }
106
-
107
- function isGuardedHost(host) {
108
- if (!host) return false;
109
- if (host === ownHost()) return false;
110
- return providerHosts().some((h) => host.includes(h));
111
- }
112
-
113
- let installed = false;
114
-
115
- /**
116
- * Patch global fetch and http(s).request so a blocked agent cannot reach any
117
- * known LLM provider, regardless of which SDK it is using. Idempotent — safe
118
- * to call more than once, and safe to call when no provider SDK is present.
119
- */
120
- export function installNetworkGuard() {
121
- if (installed) return;
122
- installed = true;
123
-
124
- if (typeof globalThis.fetch === "function" && !globalThis.fetch.__stratosGuarded) {
125
- const realFetch = globalThis.fetch.bind(globalThis);
126
- const guarded = function stratosGuardedFetch(input, init) {
127
- if (isGuardedHost(hostFromInput(input))) {
128
- const { blocked, reason } = isBlocked();
129
- if (blocked) return Promise.reject(new StratosBudgetBlockedError(reason));
130
- }
131
- return realFetch(input, init);
132
- };
133
- guarded.__stratosGuarded = true;
134
- globalThis.fetch = guarded;
135
- }
136
-
137
- patchRequestModule(http);
138
- patchRequestModule(https);
139
- }
140
-
141
- /**
142
- * Secondary guard for legacy http(s)-based clients — the fetch guard above is
143
- * the primary path and already covers current OpenAI / Anthropic / Google /
144
- * Vercel AI SDKs, which all use fetch on Node 18+. A synchronous throw here is
145
- * non-standard for a network failure (Node normally emits an async 'error' on
146
- * the returned request), but building a spec-compliant fake ClientRequest for
147
- * a fallback path most agents never exercise is not worth the complexity —
148
- * callers of http(s).request that care will have it in a try/catch already,
149
- * since the real function can also throw synchronously for invalid options.
150
- */
151
- function patchRequestModule(mod) {
152
- if (!mod || !mod.request || mod.request.__stratosGuarded) return;
153
- const real = mod.request;
154
- const guarded = function stratosGuardedRequest(...args) {
155
- const opts = args.find((a) => a && typeof a === "object" && typeof a !== "function");
156
- const host = opts ? String(opts.hostname || opts.host || "") : hostFromInput(args[0]);
157
- if (isGuardedHost(host)) {
158
- const { blocked, reason } = isBlocked();
159
- if (blocked) throw new StratosBudgetBlockedError(reason);
160
- }
161
- return real.apply(mod, args);
162
- };
163
- guarded.__stratosGuarded = true;
164
- mod.request = guarded;
165
-
166
- // P1-01: Also wrap .get() to catch https.get() and named imports like `import { get } from "node:https"`
167
- if (mod.get && !mod.get.__stratosGuarded) {
168
- const realGet = mod.get;
169
- const guardedGet = function stratosGuardedGet(...args) {
170
- const opts = args.find((a) => a && typeof a === "object" && typeof a !== "function");
171
- const host = opts ? String(opts.hostname || opts.host || "") : hostFromInput(args[0]);
172
- if (isGuardedHost(host)) {
173
- const { blocked, reason } = isBlocked();
174
- if (blocked) throw new StratosBudgetBlockedError(reason);
175
- }
176
- return realGet.apply(mod, args);
177
- };
178
- guardedGet.__stratosGuarded = true;
179
- mod.get = guardedGet;
180
- }
181
-
182
- // Ensure named imports (import { request, get } from "node:https") see wrapped versions
183
- module.syncBuiltinESMExports?.();
184
- }
1
+ // Network-level budget / lifecycle enforcement.
2
+ //
3
+ // WHY THE NETWORK LAYER, NOT THE SDK LAYER
4
+ // -----------------------------------------------------------------------------
5
+ // OpenAI, Anthropic, LangChain, Vercel AI, Google GenAI — every one of them
6
+ // ultimately does the same thing: send an outbound HTTP request to the
7
+ // provider's API. Traceloop's own instrumentation (which this SDK depends on
8
+ // but does not own — @traceloop/instrumentation-openai et al.) exposes no
9
+ // pre-call hook that can refuse a request; its config surface is
10
+ // traceContent / enrichTokens / exceptionLogger / uploadBase64Image only.
11
+ // Wrapping every provider SDK's call function individually is also a moving
12
+ // target — a "custom agent" built directly on a raw SDK gets no protection
13
+ // until someone ships a wrapper for that specific library.
14
+ //
15
+ // Patching global `fetch` (and `http`/`https` as a fallback for older
16
+ // transports) catches all of them in ONE place, before the request leaves the
17
+ // process, with no per-provider code and no dependency on Traceloop's
18
+ // internals. This module is installed from register.js, before any provider
19
+ // SDK is imported by the agent's own code — the same ordering guarantee the
20
+ // Vercel AI ESM loader already relies on.
21
+ //
22
+ // This does NOT cover the MCP proxy path (src/mcp-proxy.js): that spawns a
23
+ // separate child process and proxies its stdio, so patching fetch in THIS
24
+ // process has no effect on the child's own network calls. The proxy has its
25
+ // own, necessary check at the JSON-RPC `tools/call` boundary instead.
26
+
27
+ import http from "node:http";
28
+ import https from "node:https";
29
+
30
+ export class StratosBudgetBlockedError extends Error {
31
+ constructor(reason) {
32
+ super(`[stratos] request blocked: ${reason || "budget or lifecycle limit reached"}`);
33
+ this.name = "StratosBudgetBlockedError";
34
+ this.stratosBlocked = true;
35
+ }
36
+ }
37
+
38
+ // Cached from the heartbeat response (see index.js / mcp-proxy.js). Starts
39
+ // unblocked: an agent must never be frozen just because it hasn't heard back
40
+ // from Stratos yet — same fail-open philosophy as the server's own enforcement
41
+ // check (lib/quotaEnforce.ts: "fail open, loudly").
42
+ let _blocked = false;
43
+ let _reason = null;
44
+
45
+ export function setBlocked(blocked, reason) {
46
+ _blocked = !!blocked;
47
+ _reason = reason || null;
48
+ }
49
+
50
+ export function isBlocked() {
51
+ return { blocked: _blocked, reason: _reason };
52
+ }
53
+
54
+ // Hostnames of LLM/agent providers to guard. Extend without an SDK release via
55
+ // STRATOS_BLOCK_HOSTS (comma-separated substrings) — e.g. a private Azure
56
+ // OpenAI deployment hostname, or a self-hosted gateway.
57
+ const DEFAULT_PROVIDER_HOSTS = [
58
+ "api.openai.com",
59
+ "api.anthropic.com",
60
+ "generativelanguage.googleapis.com",
61
+ "aiplatform.googleapis.com",
62
+ "bedrock-runtime.", // *.amazonaws.com region-scoped, matched by substring
63
+ "api.cohere.ai",
64
+ "api.mistral.ai",
65
+ "openrouter.ai",
66
+ "api.together.xyz",
67
+ "api.groq.com",
68
+ "openai.azure.com",
69
+ ];
70
+
71
+ function providerHosts() {
72
+ const extra = (process.env.STRATOS_BLOCK_HOSTS || "")
73
+ .split(",")
74
+ .map((s) => s.trim())
75
+ .filter(Boolean);
76
+ return [...DEFAULT_PROVIDER_HOSTS, ...extra];
77
+ }
78
+
79
+ // Never guard Stratos's own host: the agent has to be able to reach Stratos to
80
+ // find out it has been unblocked. If this returned the wrong host the agent
81
+ // would deadlock itself — checked defensively, not just assumed.
82
+ function ownHost() {
83
+ try {
84
+ return new URL(process.env.STRATOS_URL || "http://localhost:4000").host;
85
+ } catch {
86
+ return "";
87
+ }
88
+ }
89
+
90
+ function hostFromInput(input) {
91
+ try {
92
+ if (typeof input === "string") return new URL(input).host;
93
+ if (input instanceof URL) return input.host;
94
+ if (input && typeof input === "object") {
95
+ if (input.url) return new URL(input.url).host; // Request-like
96
+ if (input.hostname || input.host) return String(input.hostname || input.host);
97
+ }
98
+ } catch {
99
+ // Not a parseable URL. Let it through — this guard only ever restricts
100
+ // requests to a known provider host, never anything it can't identify.
101
+ }
102
+ return "";
103
+ }
104
+
105
+ function isGuardedHost(host) {
106
+ if (!host) return false;
107
+ if (host === ownHost()) return false;
108
+ return providerHosts().some((h) => host.includes(h));
109
+ }
110
+
111
+ let installed = false;
112
+
113
+ /**
114
+ * Patch global fetch and http(s).request so a blocked agent cannot reach any
115
+ * known LLM provider, regardless of which SDK it is using. Idempotent — safe
116
+ * to call more than once, and safe to call when no provider SDK is present.
117
+ */
118
+ export function installNetworkGuard() {
119
+ if (installed) return;
120
+ installed = true;
121
+
122
+ if (typeof globalThis.fetch === "function" && !globalThis.fetch.__stratosGuarded) {
123
+ const realFetch = globalThis.fetch.bind(globalThis);
124
+ const guarded = function stratosGuardedFetch(input, init) {
125
+ if (isGuardedHost(hostFromInput(input))) {
126
+ const { blocked, reason } = isBlocked();
127
+ if (blocked) return Promise.reject(new StratosBudgetBlockedError(reason));
128
+ }
129
+ return realFetch(input, init);
130
+ };
131
+ guarded.__stratosGuarded = true;
132
+ globalThis.fetch = guarded;
133
+ }
134
+
135
+ patchRequestModule(http);
136
+ patchRequestModule(https);
137
+ }
138
+
139
+ /**
140
+ * Secondary guard for legacy http(s)-based clients — the fetch guard above is
141
+ * the primary path and already covers current OpenAI / Anthropic / Google /
142
+ * Vercel AI SDKs, which all use fetch on Node 18+. A synchronous throw here is
143
+ * non-standard for a network failure (Node normally emits an async 'error' on
144
+ * the returned request), but building a spec-compliant fake ClientRequest for
145
+ * a fallback path most agents never exercise is not worth the complexity —
146
+ * callers of http(s).request that care will have it in a try/catch already,
147
+ * since the real function can also throw synchronously for invalid options.
148
+ */
149
+ function patchRequestModule(mod) {
150
+ if (!mod || !mod.request || mod.request.__stratosGuarded) return;
151
+ const real = mod.request;
152
+ const guarded = function stratosGuardedRequest(...args) {
153
+ const opts = args.find((a) => a && typeof a === "object" && typeof a !== "function");
154
+ const host = opts ? String(opts.hostname || opts.host || "") : hostFromInput(args[0]);
155
+ if (isGuardedHost(host)) {
156
+ const { blocked, reason } = isBlocked();
157
+ if (blocked) throw new StratosBudgetBlockedError(reason);
158
+ }
159
+ return real.apply(mod, args);
160
+ };
161
+ guarded.__stratosGuarded = true;
162
+ mod.request = guarded;
163
+
164
+ // P1-01: Also wrap .get() to catch https.get() and named imports like `import { get } from "node:https"`
165
+ if (mod.get && !mod.get.__stratosGuarded) {
166
+ const realGet = mod.get;
167
+ const guardedGet = function stratosGuardedGet(...args) {
168
+ const opts = args.find((a) => a && typeof a === "object" && typeof a !== "function");
169
+ const host = opts ? String(opts.hostname || opts.host || "") : hostFromInput(args[0]);
170
+ if (isGuardedHost(host)) {
171
+ const { blocked, reason } = isBlocked();
172
+ if (blocked) throw new StratosBudgetBlockedError(reason);
173
+ }
174
+ return realGet.apply(mod, args);
175
+ };
176
+ guardedGet.__stratosGuarded = true;
177
+ mod.get = guardedGet;
178
+ }
179
+
180
+ // Ensure named imports (import { request, get } from "node:https") see wrapped versions
181
+ module.syncBuiltinESMExports?.();
182
+ }
package/src/index.js CHANGED
@@ -163,6 +163,10 @@ export function start(opts = {}) {
163
163
  }
164
164
  started = true;
165
165
 
166
+ // Display SDK version on app startup
167
+ const SDK_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
168
+ console.log(`[stratos] Stratos SDK v${SDK_VERSION} initialized`);
169
+
166
170
  const ingest = `${origin}/api/ingest`;
167
171
  const heartbeatUrl = `${origin}/api/heartbeat`;
168
172
 
@@ -6,6 +6,16 @@
6
6
  // (not node_modules/@intelli-1113/stratos-sdk).
7
7
 
8
8
  import { inspectLangChain, printInspectReport } from "./inspect-langchain.js";
9
+ import { readFileSync } from "node:fs";
10
+
11
+ function getSDKVersion() {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
14
+ return pkg.version;
15
+ } catch {
16
+ return "latest";
17
+ }
18
+ }
9
19
 
10
20
  try {
11
21
  if (process.env.STRATOS_SKIP_POSTINSTALL === "1") process.exit(0);
@@ -13,17 +23,28 @@ try {
13
23
  // Prefer the consumer project root
14
24
  const cwd = process.env.INIT_CWD || process.cwd();
15
25
  const report = inspectLangChain({ cwd });
16
- if (!report.hasLangChain) process.exit(0);
17
- if (report.compatible) process.exit(0);
18
-
19
- console.warn(
20
- "\n[stratos] ⚠ LangChain version is not compatible with Stratos auto-metrics."
21
- );
22
- printInspectReport(report);
23
- console.warn(
24
- "[stratos] In your project, run:\n npx stratos-inspect --fix\n"
25
- );
26
+
27
+ // Show LangChain warnings if needed
28
+ if (report.hasLangChain && !report.compatible) {
29
+ console.warn(
30
+ "\n[stratos] ⚠ LangChain version is not compatible with Stratos auto-metrics."
31
+ );
32
+ printInspectReport(report);
33
+ console.warn(
34
+ "[stratos] In your project, run:\n npx stratos-inspect --fix\n"
35
+ );
36
+ }
26
37
  } catch {
27
38
  // Never fail the host install
28
- process.exit(0);
29
39
  }
40
+
41
+ // Always show welcome message
42
+ const version = getSDKVersion();
43
+ console.log(`
44
+ ✨ Welcome to Stratos SDK v${version}!
45
+
46
+ Get started:
47
+ → Run: stratos --help
48
+
49
+ This will show all available commands and a 4-step integration guide.
50
+ `);