@openbox-ai/openbox-mastra-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/openbox-client.d.ts +42 -0
- package/dist/client/openbox-client.js +405 -0
- package/dist/client/openbox-client.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/openbox-config.d.ts +54 -0
- package/dist/config/openbox-config.js +162 -0
- package/dist/config/openbox-config.js.map +1 -0
- package/dist/governance/activity-runtime.d.ts +42 -0
- package/dist/governance/activity-runtime.js +712 -0
- package/dist/governance/activity-runtime.js.map +1 -0
- package/dist/governance/approval-registry.d.ts +17 -0
- package/dist/governance/approval-registry.js +32 -0
- package/dist/governance/approval-registry.js.map +1 -0
- package/dist/governance/context.d.ts +16 -0
- package/dist/governance/context.js +13 -0
- package/dist/governance/context.js.map +1 -0
- package/dist/governance/index.d.ts +2 -0
- package/dist/governance/index.js +1 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mastra/index.d.ts +16 -0
- package/dist/mastra/index.js +5 -0
- package/dist/mastra/index.js.map +1 -0
- package/dist/mastra/with-openbox.d.ts +30 -0
- package/dist/mastra/with-openbox.js +243 -0
- package/dist/mastra/with-openbox.js.map +1 -0
- package/dist/mastra/wrap-agent.d.ts +14 -0
- package/dist/mastra/wrap-agent.js +1744 -0
- package/dist/mastra/wrap-agent.js.map +1 -0
- package/dist/mastra/wrap-tool.d.ts +18 -0
- package/dist/mastra/wrap-tool.js +49 -0
- package/dist/mastra/wrap-tool.js.map +1 -0
- package/dist/mastra/wrap-workflow.d.ts +14 -0
- package/dist/mastra/wrap-workflow.js +386 -0
- package/dist/mastra/wrap-workflow.js.map +1 -0
- package/dist/otel/index.d.ts +11 -0
- package/dist/otel/index.js +2 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/otel/setup-openbox-opentelemetry.d.ts +38 -0
- package/dist/otel/setup-openbox-opentelemetry.js +2249 -0
- package/dist/otel/setup-openbox-opentelemetry.js.map +1 -0
- package/dist/span/index.d.ts +5 -0
- package/dist/span/index.js +2 -0
- package/dist/span/index.js.map +1 -0
- package/dist/span/openbox-span-processor.d.ts +90 -0
- package/dist/span/openbox-span-processor.js +580 -0
- package/dist/span/openbox-span-processor.js.map +1 -0
- package/dist/types/errors.d.ts +25 -0
- package/dist/types/errors.js +40 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/governance-verdict-response.d.ts +57 -0
- package/dist/types/governance-verdict-response.js +84 -0
- package/dist/types/governance-verdict-response.js.map +1 -0
- package/dist/types/guardrails.d.ts +23 -0
- package/dist/types/guardrails.js +27 -0
- package/dist/types/guardrails.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/verdict.d.ts +22 -0
- package/dist/types/verdict.js +55 -0
- package/dist/types/verdict.js.map +1 -0
- package/dist/types/workflow-event-type.d.ts +10 -0
- package/dist/types/workflow-event-type.js +13 -0
- package/dist/types/workflow-event-type.js.map +1 -0
- package/dist/types/workflow-span-buffer.d.ts +31 -0
- package/dist/types/workflow-span-buffer.js +42 -0
- package/dist/types/workflow-span-buffer.js.map +1 -0
- package/docs/README.md +66 -0
- package/docs/api-reference.md +348 -0
- package/docs/approvals-and-guardrails.md +163 -0
- package/docs/architecture.md +186 -0
- package/docs/configuration.md +214 -0
- package/docs/event-model.md +215 -0
- package/docs/installation.md +108 -0
- package/docs/integration-patterns.md +214 -0
- package/docs/security-and-privacy.md +174 -0
- package/docs/telemetry.md +196 -0
- package/docs/troubleshooting.md +210 -0
- package/package.json +136 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OpenBox Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# OpenBox Mastra SDK
|
|
2
|
+
|
|
3
|
+
`@openbox-ai/openbox-mastra-sdk` adds OpenBox governance, approvals, guardrails, and OpenTelemetry-backed operational telemetry to Mastra applications.
|
|
4
|
+
|
|
5
|
+
Use it when you need to:
|
|
6
|
+
|
|
7
|
+
- evaluate tools, workflow steps, workflows, and agents against OpenBox policy
|
|
8
|
+
- enforce approval flows from OpenBox verdicts
|
|
9
|
+
- apply input and output guardrails
|
|
10
|
+
- attach HTTP, database, file, and traced-function telemetry to governed runs
|
|
11
|
+
- install the integration once and keep future Mastra registrations governed
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js `>=24.10.0`
|
|
16
|
+
- `@mastra/core` `^1.8.0`
|
|
17
|
+
- an OpenBox Core deployment reachable from the Mastra runtime
|
|
18
|
+
- an ESM-capable runtime and build pipeline
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @openbox-ai/openbox-mastra-sdk @mastra/core
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Required environment variables:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export OPENBOX_URL="https://your-openbox-core.example"
|
|
30
|
+
export OPENBOX_API_KEY="obx_live_your_key"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Optional but commonly used:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export OPENBOX_GOVERNANCE_POLICY="fail_open"
|
|
37
|
+
export OPENBOX_DEBUG="false"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Mastra } from "@mastra/core/mastra";
|
|
44
|
+
import {
|
|
45
|
+
getOpenBoxRuntime,
|
|
46
|
+
withOpenBox
|
|
47
|
+
} from "@openbox-ai/openbox-mastra-sdk";
|
|
48
|
+
|
|
49
|
+
const mastra = new Mastra({
|
|
50
|
+
agents: {
|
|
51
|
+
// your agents
|
|
52
|
+
},
|
|
53
|
+
tools: {
|
|
54
|
+
// your tools
|
|
55
|
+
},
|
|
56
|
+
workflows: {
|
|
57
|
+
// your workflows
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const governedMastra = await withOpenBox(mastra, {
|
|
62
|
+
apiKey: process.env.OPENBOX_API_KEY,
|
|
63
|
+
apiUrl: process.env.OPENBOX_URL
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on("SIGTERM", async () => {
|
|
67
|
+
await getOpenBoxRuntime(governedMastra)?.shutdown();
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`withOpenBox()` is the recommended production entrypoint. It:
|
|
72
|
+
|
|
73
|
+
1. parses and validates SDK configuration
|
|
74
|
+
2. validates the API key unless `validate: false` is set
|
|
75
|
+
3. creates the OpenBox client and span processor
|
|
76
|
+
4. installs process-wide telemetry
|
|
77
|
+
5. wraps existing Mastra tools, workflows, and agents
|
|
78
|
+
6. patches future `addTool()`, `addWorkflow()`, and `addAgent()` calls
|
|
79
|
+
|
|
80
|
+
## Runtime Model
|
|
81
|
+
|
|
82
|
+
The SDK emits three categories of OpenBox payloads:
|
|
83
|
+
|
|
84
|
+
- boundary workflow events: `WorkflowStarted`, `WorkflowCompleted`, `WorkflowFailed`
|
|
85
|
+
- boundary activity events: `ActivityStarted`, `ActivityCompleted`
|
|
86
|
+
- signal events: `SignalReceived` for workflow resume, agent `user_input`, agent `resume`, and agent `agent_output`
|
|
87
|
+
|
|
88
|
+
It also captures operational spans for:
|
|
89
|
+
|
|
90
|
+
- HTTP requests
|
|
91
|
+
- supported database libraries
|
|
92
|
+
- file operations when file instrumentation is enabled
|
|
93
|
+
- custom functions wrapped with `traced()`
|
|
94
|
+
|
|
95
|
+
Important production behavior:
|
|
96
|
+
|
|
97
|
+
- agent-only LLM activity is represented as telemetry spans, not as standalone business activities
|
|
98
|
+
- agent prompts are emitted as `SignalReceived(user_input)`, not as tool activities
|
|
99
|
+
- the SDK ignores its own OpenBox API URL during telemetry setup to avoid feedback loops
|
|
100
|
+
|
|
101
|
+
## Configuration Highlights
|
|
102
|
+
|
|
103
|
+
Most applications only need a small part of the config surface:
|
|
104
|
+
|
|
105
|
+
| Option | Default | Use it to |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `apiUrl` | required | point the SDK at OpenBox Core |
|
|
108
|
+
| `apiKey` | required | authenticate governance and approval calls |
|
|
109
|
+
| `validate` | `true` | fail fast on invalid credentials or insecure URL setup |
|
|
110
|
+
| `onApiError` | `"fail_open"` | decide whether OpenBox outages should halt execution |
|
|
111
|
+
| `hitlEnabled` | `true` | enable approval suspension or polling flows |
|
|
112
|
+
| `httpCapture` | `true` | attach text HTTP bodies and headers to governance-relevant telemetry |
|
|
113
|
+
| `instrumentDatabases` | `true` | capture supported database activity |
|
|
114
|
+
| `instrumentFileIo` | `false` | enable file operation telemetry when required |
|
|
115
|
+
| `sendStartEvent` | `true` | emit `WorkflowStarted` |
|
|
116
|
+
| `sendActivityStartEvent` | `true` | emit `ActivityStarted` |
|
|
117
|
+
| `skipActivityTypes` | `["send_governance_event"]` | suppress selected activity types entirely |
|
|
118
|
+
| `skipSignals` | empty | suppress selected signal names |
|
|
119
|
+
| `maxEvaluatePayloadBytes` | `256000` | cap payload size before compact fallback logic applies |
|
|
120
|
+
|
|
121
|
+
See [docs/configuration.md](./docs/configuration.md) for the complete surface.
|
|
122
|
+
|
|
123
|
+
## Production Guidance
|
|
124
|
+
|
|
125
|
+
- Keep `validate` enabled outside tests and local mocks.
|
|
126
|
+
- Use HTTPS for all non-localhost OpenBox endpoints.
|
|
127
|
+
- Decide explicitly between `fail_open` and `fail_closed` before deployment.
|
|
128
|
+
- Treat hook-triggered telemetry as internal operational data unless your policy intentionally governs it.
|
|
129
|
+
- Keep `instrumentFileIo` disabled until you have a concrete file-governance requirement.
|
|
130
|
+
- Initialize telemetry once per process and shut it down on process exit.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- [docs/README.md](./docs/README.md): documentation index and reading order
|
|
135
|
+
- [docs/installation.md](./docs/installation.md): installation, startup, and shutdown
|
|
136
|
+
- [docs/configuration.md](./docs/configuration.md): configuration surface, env vars, and defaults
|
|
137
|
+
- [docs/integration-patterns.md](./docs/integration-patterns.md): `withOpenBox()` and manual integration patterns
|
|
138
|
+
- [docs/architecture.md](./docs/architecture.md): runtime architecture and data flow
|
|
139
|
+
- [docs/event-model.md](./docs/event-model.md): event types, payload shape, signals, and activity semantics
|
|
140
|
+
- [docs/telemetry.md](./docs/telemetry.md): HTTP, database, file, and traced-function capture
|
|
141
|
+
- [docs/approvals-and-guardrails.md](./docs/approvals-and-guardrails.md): verdict enforcement, approvals, and guardrails
|
|
142
|
+
- [docs/security-and-privacy.md](./docs/security-and-privacy.md): transport, capture boundaries, and hardening guidance
|
|
143
|
+
- [docs/troubleshooting.md](./docs/troubleshooting.md): common failures and diagnostics
|
|
144
|
+
- [docs/api-reference.md](./docs/api-reference.md): public API summary
|
|
145
|
+
|
|
146
|
+
## Public API Summary
|
|
147
|
+
|
|
148
|
+
Top-level exports include:
|
|
149
|
+
|
|
150
|
+
- `withOpenBox()` and `getOpenBoxRuntime()`
|
|
151
|
+
- `wrapTool()`, `wrapWorkflow()`, and `wrapAgent()`
|
|
152
|
+
- `OpenBoxClient`
|
|
153
|
+
- `parseOpenBoxConfig()` and `initializeOpenBox()`
|
|
154
|
+
- `setupOpenBoxOpenTelemetry()` and `traced()`
|
|
155
|
+
- `OpenBoxSpanProcessor`
|
|
156
|
+
- verdict, guardrail, workflow event, and error types
|
|
157
|
+
|
|
158
|
+
See [docs/api-reference.md](./docs/api-reference.md) for the full reference.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"sourcesContent":["export * from \"./openbox-client.js\";\n"],"mappings":"AAAA,cAAc;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { GovernanceVerdictResponse } from '../types/governance-verdict-response.js';
|
|
2
|
+
import '../types/guardrails.js';
|
|
3
|
+
import '../types/verdict.js';
|
|
4
|
+
|
|
5
|
+
type OpenBoxApiErrorPolicy = "fail_open" | "fail_closed";
|
|
6
|
+
interface OpenBoxClientOptions {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
evaluateMaxRetries?: number | undefined;
|
|
10
|
+
evaluateRetryBaseDelayMs?: number | undefined;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
onApiError?: OpenBoxApiErrorPolicy | undefined;
|
|
13
|
+
timeoutSeconds?: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
interface ApprovalPollRequest {
|
|
16
|
+
activityId: string;
|
|
17
|
+
runId: string;
|
|
18
|
+
workflowId: string;
|
|
19
|
+
}
|
|
20
|
+
interface ApprovalPollResponse {
|
|
21
|
+
action?: string | undefined;
|
|
22
|
+
approval_expiration_time?: string | null | undefined;
|
|
23
|
+
expired?: boolean | undefined;
|
|
24
|
+
reason?: string | undefined;
|
|
25
|
+
verdict?: string | undefined;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
declare class OpenBoxClient {
|
|
29
|
+
#private;
|
|
30
|
+
readonly apiKey: string;
|
|
31
|
+
readonly apiUrl: string;
|
|
32
|
+
readonly evaluateMaxRetries: number;
|
|
33
|
+
readonly evaluateRetryBaseDelayMs: number;
|
|
34
|
+
readonly onApiError: OpenBoxApiErrorPolicy;
|
|
35
|
+
readonly timeoutSeconds: number;
|
|
36
|
+
constructor({ apiKey, apiUrl, evaluateMaxRetries, evaluateRetryBaseDelayMs, fetch: customFetch, onApiError, timeoutSeconds }: OpenBoxClientOptions);
|
|
37
|
+
validateApiKey(): Promise<void>;
|
|
38
|
+
evaluate(payload: Record<string, unknown>): Promise<GovernanceVerdictResponse | null>;
|
|
39
|
+
pollApproval(payload: ApprovalPollRequest): Promise<ApprovalPollResponse | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { type ApprovalPollRequest, type ApprovalPollResponse, type OpenBoxApiErrorPolicy, OpenBoxClient, type OpenBoxClientOptions };
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GovernanceAPIError,
|
|
3
|
+
GovernanceVerdictResponse,
|
|
4
|
+
OpenBoxAuthError,
|
|
5
|
+
OpenBoxNetworkError
|
|
6
|
+
} from "../types/index.js";
|
|
7
|
+
const USER_AGENT = "OpenBox-SDK/1.0";
|
|
8
|
+
class OpenBoxClient {
|
|
9
|
+
apiKey;
|
|
10
|
+
apiUrl;
|
|
11
|
+
evaluateMaxRetries;
|
|
12
|
+
evaluateRetryBaseDelayMs;
|
|
13
|
+
onApiError;
|
|
14
|
+
timeoutSeconds;
|
|
15
|
+
#fetch;
|
|
16
|
+
#debugEnabled;
|
|
17
|
+
constructor({
|
|
18
|
+
apiKey,
|
|
19
|
+
apiUrl,
|
|
20
|
+
evaluateMaxRetries = 0,
|
|
21
|
+
evaluateRetryBaseDelayMs = 150,
|
|
22
|
+
fetch: customFetch,
|
|
23
|
+
onApiError = "fail_open",
|
|
24
|
+
timeoutSeconds = 30
|
|
25
|
+
}) {
|
|
26
|
+
this.apiKey = apiKey;
|
|
27
|
+
this.apiUrl = apiUrl.replace(/\/+$/, "");
|
|
28
|
+
this.evaluateMaxRetries = Math.max(0, Math.floor(evaluateMaxRetries));
|
|
29
|
+
this.evaluateRetryBaseDelayMs = Math.max(0, Math.floor(evaluateRetryBaseDelayMs));
|
|
30
|
+
this.onApiError = onApiError;
|
|
31
|
+
this.timeoutSeconds = timeoutSeconds;
|
|
32
|
+
this.#fetch = customFetch ?? fetch;
|
|
33
|
+
this.#debugEnabled = isOpenBoxDebugEnabled();
|
|
34
|
+
}
|
|
35
|
+
async validateApiKey() {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.#fetch(
|
|
38
|
+
this.#buildUrl("/api/v1/auth/validate"),
|
|
39
|
+
{
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"User-Agent": USER_AGENT
|
|
44
|
+
},
|
|
45
|
+
method: "GET",
|
|
46
|
+
signal: AbortSignal.timeout(this.timeoutSeconds * 1e3)
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
if (response.status === 200) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 401 || response.status === 403) {
|
|
53
|
+
throw new OpenBoxAuthError(
|
|
54
|
+
"Invalid API key. Check your API key at dashboard.openbox.ai"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
throw new OpenBoxNetworkError(
|
|
58
|
+
`Cannot reach OpenBox Core at ${this.apiUrl}: HTTP ${response.status}`
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof OpenBoxAuthError || error instanceof OpenBoxNetworkError) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
throw new OpenBoxNetworkError(
|
|
65
|
+
`Cannot reach OpenBox Core at ${this.apiUrl}: ${this.#errorMessage(error)}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async evaluate(payload) {
|
|
70
|
+
const normalizedPayload = normalizeEvaluatePayload(payload);
|
|
71
|
+
return this.#withApiPolicy(
|
|
72
|
+
async () => this.#evaluateWithRetry(normalizedPayload)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
async pollApproval(payload) {
|
|
76
|
+
try {
|
|
77
|
+
if (this.#debugEnabled) {
|
|
78
|
+
console.info("[openbox-sdk] approval.request", {
|
|
79
|
+
activity_id: payload.activityId,
|
|
80
|
+
run_id: payload.runId,
|
|
81
|
+
workflow_id: payload.workflowId
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const response = await this.#fetch(
|
|
85
|
+
this.#buildUrl("/api/v1/governance/approval"),
|
|
86
|
+
{
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
activity_id: payload.activityId,
|
|
89
|
+
run_id: payload.runId,
|
|
90
|
+
workflow_id: payload.workflowId
|
|
91
|
+
}),
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"User-Agent": USER_AGENT
|
|
96
|
+
},
|
|
97
|
+
method: "POST",
|
|
98
|
+
signal: AbortSignal.timeout(this.timeoutSeconds * 1e3)
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
if (response.status !== 200) {
|
|
102
|
+
const body = await response.text().catch(() => "");
|
|
103
|
+
if (this.#debugEnabled) {
|
|
104
|
+
console.error("[openbox-sdk] approval.response", {
|
|
105
|
+
reason: body,
|
|
106
|
+
status: response.status
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const data = await response.json();
|
|
112
|
+
if (this.#debugEnabled) {
|
|
113
|
+
console.info("[openbox-sdk] approval.response", {
|
|
114
|
+
action: data.action,
|
|
115
|
+
status: response.status,
|
|
116
|
+
verdict: data.verdict
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const expirationTime = data.approval_expiration_time;
|
|
120
|
+
if (typeof expirationTime === "string") {
|
|
121
|
+
const parsed = parseApprovalExpiration(expirationTime);
|
|
122
|
+
if (parsed && Date.now() > parsed.getTime()) {
|
|
123
|
+
return {
|
|
124
|
+
...data,
|
|
125
|
+
expired: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return data;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
#buildUrl(pathname) {
|
|
135
|
+
return `${this.apiUrl}${pathname}`;
|
|
136
|
+
}
|
|
137
|
+
async #evaluateWithRetry(payload) {
|
|
138
|
+
let attempt = 0;
|
|
139
|
+
while (true) {
|
|
140
|
+
try {
|
|
141
|
+
return await this.#evaluateOnce(payload);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (attempt >= this.evaluateMaxRetries || !isRetryableEvaluateError(error)) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const waitMs = this.evaluateRetryBaseDelayMs * 2 ** attempt;
|
|
147
|
+
if (this.#debugEnabled) {
|
|
148
|
+
console.warn("[openbox-sdk] evaluate.retry", {
|
|
149
|
+
attempt: attempt + 1,
|
|
150
|
+
error: error instanceof Error ? error.message : String(error),
|
|
151
|
+
wait_ms: waitMs
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
attempt += 1;
|
|
155
|
+
if (waitMs > 0) {
|
|
156
|
+
await delay(waitMs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async #evaluateOnce(payload) {
|
|
162
|
+
if (this.#debugEnabled) {
|
|
163
|
+
console.info("[openbox-sdk] evaluate.request", summarizeEvaluatePayload(payload));
|
|
164
|
+
}
|
|
165
|
+
const response = await this.#fetch(
|
|
166
|
+
this.#buildUrl("/api/v1/governance/evaluate"),
|
|
167
|
+
{
|
|
168
|
+
body: JSON.stringify(payload),
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
"User-Agent": USER_AGENT
|
|
173
|
+
},
|
|
174
|
+
method: "POST",
|
|
175
|
+
signal: AbortSignal.timeout(this.timeoutSeconds * 1e3)
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
if (response.status !== 200) {
|
|
179
|
+
const body = await response.text();
|
|
180
|
+
if (this.#debugEnabled) {
|
|
181
|
+
console.error("[openbox-sdk] evaluate.response", {
|
|
182
|
+
event_type: payload.event_type,
|
|
183
|
+
reason: body,
|
|
184
|
+
status: response.status
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
throw new GovernanceAPIError(
|
|
188
|
+
`HTTP ${response.status}: ${body}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const parsed = await response.json();
|
|
192
|
+
if (this.#debugEnabled) {
|
|
193
|
+
const ageResult = parsed && typeof parsed === "object" && "age_result" in parsed ? parsed.age_result : void 0;
|
|
194
|
+
console.info("[openbox-sdk] evaluate.response", {
|
|
195
|
+
action: parsed.action,
|
|
196
|
+
age_fallback_used: ageResult && typeof ageResult === "object" ? ageResult.fallback_used : void 0,
|
|
197
|
+
age_goal_alignment_checked: ageResult && typeof ageResult === "object" ? ageResult.goal_alignment_checked : void 0,
|
|
198
|
+
age_goal_drifted: ageResult && typeof ageResult === "object" ? ageResult.goal_drifted : void 0,
|
|
199
|
+
event_type: payload.event_type,
|
|
200
|
+
status: response.status,
|
|
201
|
+
verdict: parsed.verdict
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return GovernanceVerdictResponse.fromObject(parsed);
|
|
205
|
+
}
|
|
206
|
+
async #withApiPolicy(operation) {
|
|
207
|
+
try {
|
|
208
|
+
return await operation();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (this.onApiError === "fail_open") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (error instanceof GovernanceAPIError) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
throw new GovernanceAPIError(this.#errorMessage(error));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
#errorMessage(error) {
|
|
220
|
+
if (error instanceof Error) {
|
|
221
|
+
return error.message;
|
|
222
|
+
}
|
|
223
|
+
return String(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function isOpenBoxDebugEnabled() {
|
|
227
|
+
const value = process.env.OPENBOX_DEBUG?.trim().toLowerCase();
|
|
228
|
+
return value === "1" || value === "true" || value === "yes";
|
|
229
|
+
}
|
|
230
|
+
function summarizeEvaluatePayload(payload) {
|
|
231
|
+
const spanSummary = summarizeSpans(payload.spans);
|
|
232
|
+
return {
|
|
233
|
+
activity_id: payload.activity_id,
|
|
234
|
+
activity_type: payload.activity_type,
|
|
235
|
+
event_type: payload.event_type,
|
|
236
|
+
has_activity_input: payload.activity_input !== void 0,
|
|
237
|
+
has_activity_output: payload.activity_output !== void 0,
|
|
238
|
+
has_error: payload.error !== void 0,
|
|
239
|
+
has_goal: typeof payload.goal === "string" && payload.goal.trim().length > 0,
|
|
240
|
+
has_signal_args: payload.signal_args !== void 0,
|
|
241
|
+
has_spans: spanSummary.hasSpans,
|
|
242
|
+
has_workflow_input: payload.workflow_input !== void 0,
|
|
243
|
+
has_workflow_output: payload.workflow_output !== void 0,
|
|
244
|
+
hook_stage: payload.hook_trigger === true ? spanSummary.latestSpanStage : void 0,
|
|
245
|
+
run_id: payload.run_id,
|
|
246
|
+
span_count: typeof payload.span_count === "number" ? payload.span_count : spanSummary.detectedSpanCount,
|
|
247
|
+
synthetic_model_usage_span: spanSummary.syntheticModelUsageSpan,
|
|
248
|
+
workflow_model_id: typeof payload.model_id === "string" ? payload.model_id : void 0,
|
|
249
|
+
workflow_model_provider: typeof payload.model_provider === "string" ? payload.model_provider : typeof payload.provider === "string" ? payload.provider : void 0,
|
|
250
|
+
workflow_id: payload.workflow_id,
|
|
251
|
+
workflow_type: payload.workflow_type
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function normalizeEvaluatePayload(payload) {
|
|
255
|
+
const normalized = { ...payload };
|
|
256
|
+
const eventType = typeof normalized.event_type === "string" ? normalized.event_type : void 0;
|
|
257
|
+
const legacyHookSpan = extractLegacyHookSpanFromTrigger(normalized.hook_trigger);
|
|
258
|
+
const normalizedHookTrigger = normalizeHookTrigger(normalized.hook_trigger);
|
|
259
|
+
if (normalizedHookTrigger !== void 0) {
|
|
260
|
+
normalized.hook_trigger = normalizedHookTrigger;
|
|
261
|
+
}
|
|
262
|
+
if (eventType === "ActivityCompleted") {
|
|
263
|
+
const normalizedSpans = normalizeSpansField(normalized.spans) ?? (legacyHookSpan ? [legacyHookSpan] : void 0);
|
|
264
|
+
if (normalizedHookTrigger === true || legacyHookSpan !== void 0) {
|
|
265
|
+
normalized.hook_trigger = true;
|
|
266
|
+
normalized.spans = normalizedSpans ?? [];
|
|
267
|
+
normalized.span_count = normalized.spans.length;
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
delete normalized.spans;
|
|
271
|
+
delete normalized.hook_trigger;
|
|
272
|
+
normalized.span_count = 0;
|
|
273
|
+
return normalized;
|
|
274
|
+
}
|
|
275
|
+
if (eventType === "ActivityStarted") {
|
|
276
|
+
const normalizedSpans = normalizeSpansField(normalized.spans) ?? (legacyHookSpan ? [legacyHookSpan] : void 0);
|
|
277
|
+
if (normalizedSpans !== void 0) {
|
|
278
|
+
normalized.spans = normalizedSpans;
|
|
279
|
+
normalized.span_count = normalizedSpans.length;
|
|
280
|
+
return normalized;
|
|
281
|
+
}
|
|
282
|
+
if (normalized.hook_trigger === true) {
|
|
283
|
+
normalized.spans = [];
|
|
284
|
+
normalized.span_count = 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return normalized;
|
|
288
|
+
}
|
|
289
|
+
function normalizeHookTrigger(value) {
|
|
290
|
+
if (value === void 0) {
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === "boolean") {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
if (typeof value === "number") {
|
|
297
|
+
return value !== 0;
|
|
298
|
+
}
|
|
299
|
+
if (typeof value === "string") {
|
|
300
|
+
const normalized = value.trim().toLowerCase();
|
|
301
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off" || normalized.length === 0) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (typeof value === "object") {
|
|
309
|
+
return value !== null;
|
|
310
|
+
}
|
|
311
|
+
return Boolean(value);
|
|
312
|
+
}
|
|
313
|
+
function extractLegacyHookSpanFromTrigger(value) {
|
|
314
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
const record = value;
|
|
318
|
+
const hasHookShape = typeof record.type === "string" || typeof record.hook_type === "string" || typeof record.stage === "string" || typeof record.method === "string" || typeof record.url === "string" || typeof record.http_method === "string" || typeof record.http_url === "string" || typeof record.db_operation === "string" || typeof record.db_statement === "string" || typeof record.file_operation === "string" || typeof record.file_path === "string" || typeof record.function === "string";
|
|
319
|
+
if (!hasHookShape) {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
const normalized = {
|
|
323
|
+
...record
|
|
324
|
+
};
|
|
325
|
+
if (typeof normalized.type === "string" && typeof normalized.hook_type !== "string") {
|
|
326
|
+
normalized.hook_type = normalized.type;
|
|
327
|
+
}
|
|
328
|
+
delete normalized.type;
|
|
329
|
+
return normalized;
|
|
330
|
+
}
|
|
331
|
+
function normalizeSpansField(value) {
|
|
332
|
+
if (value === void 0) {
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
return value.filter(
|
|
337
|
+
(span) => span !== null && typeof span === "object"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (value !== null && typeof value === "object") {
|
|
341
|
+
return [value];
|
|
342
|
+
}
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
function summarizeSpans(spans) {
|
|
346
|
+
if (!Array.isArray(spans)) {
|
|
347
|
+
return {
|
|
348
|
+
detectedSpanCount: 0,
|
|
349
|
+
hasSpans: false,
|
|
350
|
+
latestSpanStage: void 0,
|
|
351
|
+
syntheticModelUsageSpan: false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const spanList = spans;
|
|
355
|
+
const latestSpan = spanList.length > 0 ? spanList[spanList.length - 1] : void 0;
|
|
356
|
+
const latestSpanStage = latestSpan && typeof latestSpan === "object" ? (() => {
|
|
357
|
+
const stage = latestSpan.stage;
|
|
358
|
+
return typeof stage === "string" ? stage : void 0;
|
|
359
|
+
})() : void 0;
|
|
360
|
+
return {
|
|
361
|
+
detectedSpanCount: spanList.length,
|
|
362
|
+
hasSpans: spanList.length > 0,
|
|
363
|
+
latestSpanStage,
|
|
364
|
+
syntheticModelUsageSpan: spanList.some((span) => {
|
|
365
|
+
if (!span || typeof span !== "object") {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
return span.name === "openbox.synthetic.model_usage";
|
|
369
|
+
})
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function isRetryableEvaluateError(error) {
|
|
373
|
+
if (error instanceof GovernanceAPIError) {
|
|
374
|
+
if (/HTTP\s(429|5\d\d)\b/i.test(error.message)) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return /(context deadline exceeded|temporarily unavailable|timeout|timed out|connection reset|econnreset|etimedout|upstream connect error)/i.test(
|
|
378
|
+
error.message
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!(error instanceof Error)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (error.name === "AbortError") {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
return /(fetch failed|network|econnreset|etimedout|connection reset)/i.test(
|
|
388
|
+
error.message
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function delay(ms) {
|
|
392
|
+
return new Promise((resolve) => {
|
|
393
|
+
setTimeout(resolve, ms);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function parseApprovalExpiration(value) {
|
|
397
|
+
const normalized = value.replace(" ", "T").replace(/Z$/, "+00:00");
|
|
398
|
+
const withTimezone = /([+-]\d{2}:\d{2})$/.test(normalized) ? normalized : `${normalized}Z`;
|
|
399
|
+
const parsed = new Date(withTimezone);
|
|
400
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
401
|
+
}
|
|
402
|
+
export {
|
|
403
|
+
OpenBoxClient
|
|
404
|
+
};
|
|
405
|
+
//# sourceMappingURL=openbox-client.js.map
|