@swfte/nexus-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 +201 -0
- package/NOTICE +38 -0
- package/README.md +414 -0
- package/ai.d.ts +84 -0
- package/index.d.ts +433 -0
- package/otel.d.ts +80 -0
- package/package.json +93 -0
- package/policy.d.ts +141 -0
- package/src/ai.cjs +334 -0
- package/src/ai.js +39 -0
- package/src/core.cjs +2411 -0
- package/src/health.cjs +172 -0
- package/src/index.cjs +53 -0
- package/src/index.js +151 -0
- package/src/otel/bridge.cjs +257 -0
- package/src/otel/classify.cjs +166 -0
- package/src/otel/index.cjs +84 -0
- package/src/otel/index.js +39 -0
- package/src/otel/semconv.cjs +650 -0
- package/src/policy/engine.cjs +368 -0
- package/src/policy/envelope.cjs +256 -0
- package/src/policy/index.cjs +224 -0
- package/src/policy/rules.cjs +442 -0
- package/src/pricing.cjs +188 -0
- package/src/provenance.cjs +304 -0
- package/src/redact.cjs +734 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Swfte AI
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@swfte/nexus-sdk (Node)
|
|
2
|
+
Copyright 2026 Swfte AI
|
|
3
|
+
|
|
4
|
+
This product includes software developed at Swfte AI (https://swfte.com/).
|
|
5
|
+
|
|
6
|
+
================================================================================
|
|
7
|
+
Third-party components redistributed inside this package
|
|
8
|
+
================================================================================
|
|
9
|
+
|
|
10
|
+
None.
|
|
11
|
+
|
|
12
|
+
That is not an oversight and it is not a placeholder. The published tarball
|
|
13
|
+
contains no vendored code, no bundled dependency, and no `dependencies` entry —
|
|
14
|
+
`npm ls --omit=dev --all` on an install of this package resolves to nothing but
|
|
15
|
+
this package. `test/packaging.test.mjs` asserts it, so the claim cannot regress
|
|
16
|
+
into a stale sentence in a NOTICE file that nobody re-reads.
|
|
17
|
+
|
|
18
|
+
The Python sibling (`swfte-nexus-sdk`) *does* vendor two trees — `wrapt` and
|
|
19
|
+
dd-trace-py's import machinery — because CPython's import-hook story has no
|
|
20
|
+
usable stdlib equivalent. Node's does: `AsyncLocalStorage`, `node:http` and
|
|
21
|
+
`crypto.randomUUID` are all builtins, and the one thing that would have required
|
|
22
|
+
a third-party dependency in Node — `import-in-the-middle`, for module-hook
|
|
23
|
+
auto-instrumentation — is deliberately not shipped (see README, "What this does
|
|
24
|
+
not do"). The empty section below is a direct consequence of that decision
|
|
25
|
+
rather than a coincidence, and if module hooks are ever shipped this file gains
|
|
26
|
+
an entry in the same commit.
|
|
27
|
+
|
|
28
|
+
================================================================================
|
|
29
|
+
Development-only material, not distributed
|
|
30
|
+
================================================================================
|
|
31
|
+
|
|
32
|
+
`fixtures/vercel/` and `fixtures/oss/` install third-party packages (`ai`,
|
|
33
|
+
`zod`, `@anthropic-ai/sdk`, OpenTelemetry and Traceloop instrumentation) in
|
|
34
|
+
their own `node_modules` trees in order to measure how those libraries behave.
|
|
35
|
+
None of it is a dependency of the SDK, none of it is imported by anything under
|
|
36
|
+
`src/`, and none of it is present in the published tarball — the `files`
|
|
37
|
+
allowlist in package.json and `scripts/npm-guard.js` both exclude the fixture
|
|
38
|
+
trees, and the guard fails the publish if that ever stops being true.
|
package/README.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# @swfte/nexus-sdk
|
|
2
|
+
|
|
3
|
+
**Agent observability and enforcement for Node services you run yourself.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@swfte/nexus-sdk)
|
|
6
|
+
[](https://www.npmjs.com/package/@swfte/nexus-sdk)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](#why-no-dependencies)
|
|
9
|
+
|
|
10
|
+
`nexus wrap` captures what an agent does inside a developer's terminal. This package captures the
|
|
11
|
+
same thing inside your own Node application — a third attach point on one event ledger, stamped
|
|
12
|
+
`producer="sdk"` so a run in production and a run on a laptop are the same shape in the same
|
|
13
|
+
tables.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm install @swfte/nexus-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Node 18+. **Zero dependencies** — see [Why no dependencies](#why-no-dependencies). ESM and CommonJS
|
|
20
|
+
both supported, from one instance.
|
|
21
|
+
|
|
22
|
+
Part of **[Nexus by Swfte](https://www.swfte.com)** — savings, governance and a real audit trail
|
|
23
|
+
for AI agents. This is the Node sibling of
|
|
24
|
+
**[`swfte-nexus-sdk`](https://github.com/SwfteAI/nexus-sdk)** for Python: same concepts, same event
|
|
25
|
+
shapes, same collector. See also the
|
|
26
|
+
**[terminal wrapper](https://www.npmjs.com/package/@swfte/nexus)** (`npm i -g @swfte/nexus`).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Contents
|
|
31
|
+
|
|
32
|
+
| Section | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| [Start here, if you are bundling](#start-here-if-you-are-bundling) | read this before anything else if you deploy a bundle |
|
|
35
|
+
| [Three levels, pick one](#three-levels-pick-one) | the whole API, in increasing detail |
|
|
36
|
+
| [Guarantees](#guarantees) | what this SDK promises never to do to your process |
|
|
37
|
+
| [Configuration](#configuration) | every variable and the precedence rule |
|
|
38
|
+
| [Privacy tiers](#privacy-tiers) | what actually goes on the wire, with a worked example |
|
|
39
|
+
| [OpenTelemetry](#opentelemetry) | the OTLP bridge, and why it is a bridge |
|
|
40
|
+
| [Enforcement](#enforcement) | the part that can say *no* |
|
|
41
|
+
| [What this does not do](#what-this-does-not-do) | honestly, and with reasons |
|
|
42
|
+
| [Why no dependencies](#why-no-dependencies) | and what that costs us instead |
|
|
43
|
+
| [Development](#development) | running the suite, the layout |
|
|
44
|
+
|
|
45
|
+
**Reference documentation**
|
|
46
|
+
|
|
47
|
+
| Document | For |
|
|
48
|
+
|---|---|
|
|
49
|
+
| [`docs/API.md`](docs/API.md) | every export, its signature, and what it returns |
|
|
50
|
+
| [`PARITY.md`](PARITY.md) | line-by-line account of what matches the Python SDK, what deliberately does not, and why |
|
|
51
|
+
| [`SCOPE.md`](SCOPE.md) | the original feasibility spike — historical, kept for the record |
|
|
52
|
+
| [`AGENTS.md`](AGENTS.md) | orientation for coding agents working in this repository |
|
|
53
|
+
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | how to run the suite and what review will ask |
|
|
54
|
+
| [`SECURITY.md`](SECURITY.md) | reporting a vulnerability |
|
|
55
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | what changed, and why |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Start here, if you are bundling
|
|
60
|
+
|
|
61
|
+
Most Node advice about telemetry assumes it can patch your imports at startup. **In a bundled
|
|
62
|
+
application that does not work, and it fails silently**, so it is worth knowing before you choose
|
|
63
|
+
anything:
|
|
64
|
+
|
|
65
|
+
| | ESM app | CJS app | bundled (esbuild / webpack / ncc / Next.js) |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| Module hooks (`--import`, `--require`) | works | works | **silently captures nothing** |
|
|
68
|
+
| The Vercel AI SDK seam | works | works | **works** |
|
|
69
|
+
| The explicit API | works | works | **works** |
|
|
70
|
+
|
|
71
|
+
A bundler inlines your dependencies into your own file, so there is no `import '@anthropic-ai/sdk'`
|
|
72
|
+
left for a hook to intercept — the module boundary the technique needs no longer exists in the
|
|
73
|
+
artifact that runs. It is not degraded, it is absent, and worse: a bundled app carries its own
|
|
74
|
+
inlined copy of the SDK, so events would belong to a different session from the app's own runs.
|
|
75
|
+
|
|
76
|
+
Next.js bundles server code by default. That makes the deployment shape most likely to be running
|
|
77
|
+
an agent in Node exactly the shape module hooks cannot serve.
|
|
78
|
+
|
|
79
|
+
So this SDK is built on the two columns that survive. It ships no module hooks, and it does not
|
|
80
|
+
offer zero-code capture in Node. When nothing is capturing, it says so — `instrumentation()`
|
|
81
|
+
returns `"none"` and every `pipeline_health` record carries it. An empty dashboard that looks
|
|
82
|
+
plausible is worse than an error.
|
|
83
|
+
|
|
84
|
+
The measurements behind all of this are in [`SCOPE.md`](./SCOPE.md), and reproducible.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Three levels, pick one
|
|
89
|
+
|
|
90
|
+
**Level 1 — declare who you are.** One call, at startup:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import * as nexus from '@swfte/nexus-sdk';
|
|
94
|
+
|
|
95
|
+
nexus.init({ service: 'checkout', env: 'prod', version: '2026.8.1' });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Datadog's unified tagging. `service` / `env` / `version` land on every event as join keys. There is
|
|
99
|
+
no user identity anywhere in this SDK: a service does not need one, and inventing one is how a
|
|
100
|
+
telemetry pipeline becomes a compliance problem.
|
|
101
|
+
|
|
102
|
+
**Level 2 — capture your model calls.** If you use the Vercel AI SDK, one more call:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { registerTelemetry } from 'ai';
|
|
106
|
+
import { telemetry } from '@swfte/nexus-sdk/ai';
|
|
107
|
+
|
|
108
|
+
registerTelemetry(telemetry());
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Token counts with the cache-read/cache-write split, model and provider ids, tool names and
|
|
112
|
+
durations, and exact cost from the cache split — with no changes at your call sites, and it survives
|
|
113
|
+
bundling. Note the direction: you
|
|
114
|
+
import `ai`, not us. This package has no dependency on it and no version range to conflict with
|
|
115
|
+
yours.
|
|
116
|
+
|
|
117
|
+
**Level 3 — say what the agent did.**
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
await nexus.withAgent('refund-request', { goalClass: 'transaction' }, async (run) => {
|
|
121
|
+
const act = run.action('db.write', 'refunds');
|
|
122
|
+
act.effect({ rows: 1, amountCents: 4200 }).end();
|
|
123
|
+
|
|
124
|
+
run.usage({ model: 'claude-opus-4', inputTokens: 1800, outputTokens: 240 });
|
|
125
|
+
run.outcome('success', { verified: true, verifiedBy: 'ledger_balance' });
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A **run** is a unit of agent work with an outcome. An **action** is something with an effect on the
|
|
130
|
+
world — the thing enforcement gates. The distinction is not stylistic: what an agent *did*
|
|
131
|
+
(`behavior_trace`) and what a model *said it did* (`rationalisation`) are different epistemic
|
|
132
|
+
classes, stamped as such, and only one is admissible as evidence.
|
|
133
|
+
|
|
134
|
+
Prefer `withAgent` to `agent()` in async code. The current run propagates across `await` correctly
|
|
135
|
+
there and only there (`AsyncLocalStorage`); the object returned by `agent()` cannot do that, and
|
|
136
|
+
`action()` several frames down will not find it.
|
|
137
|
+
|
|
138
|
+
All three levels compose.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Guarantees
|
|
143
|
+
|
|
144
|
+
**A telemetry SDK must never be the reason a request fails.** Concretely, each of these is a test:
|
|
145
|
+
|
|
146
|
+
- Every public entry point is wrapped in a guard that contains exceptions and returns a safe
|
|
147
|
+
default. A guarded failure hands back an inert proxy rather than `undefined`, so host code
|
|
148
|
+
written against a `Run` keeps running instead of failing two lines later on
|
|
149
|
+
`cannot read property 'outcome' of undefined`.
|
|
150
|
+
- The calling thread never performs I/O. `flush()` is asynchronous because there is no honest
|
|
151
|
+
synchronous form to offer.
|
|
152
|
+
- The queue is bounded and drops **oldest**, because the newest events describe the incident. Every
|
|
153
|
+
drop is counted and reported in `counters()`. Silence about dropped data is a bug.
|
|
154
|
+
- Shutdown flushes to a **deadline**. A hung collector must not hang your container — asserted by a
|
|
155
|
+
test with a server that accepts the request and never answers.
|
|
156
|
+
- `SIGKILL` loses the in-flight buffer. That is not fixable and we do not pretend otherwise.
|
|
157
|
+
|
|
158
|
+
### The kill switch is real
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
NEXUS_ENABLED=0
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
No timers, no sink opened, no exit handler, no socket. `NEXUS_ENABLED=0` beats
|
|
165
|
+
`init({ enabled: true })` — an operator disabling telemetry from outside the process has to beat
|
|
166
|
+
what the application says, or it is not a kill switch.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Configuration
|
|
171
|
+
|
|
172
|
+
Explicit arguments win over environment variables, which win over defaults. The one exception is
|
|
173
|
+
`NEXUS_ENABLED=0`, which wins over everything.
|
|
174
|
+
|
|
175
|
+
| Variable | Default | |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `NEXUS_ENABLED` | `1` | `0` disables entirely |
|
|
178
|
+
| `NEXUS_SERVICE` / `NEXUS_ENV` / `NEXUS_VERSION` | `unknown` | unified tagging; the service name also falls back to `OTEL_SERVICE_NAME` / `DD_SERVICE` / `K_SERVICE` / `AWS_LAMBDA_FUNCTION_NAME` |
|
|
179
|
+
| `NEXUS_COLLECTOR_URL` | `http://127.0.0.1:8791` | full URL; `none` for no collector |
|
|
180
|
+
| `NEXUS_COLLECTOR_HOST` / `_PORT` / `_SCHEME` | — | **sidecar mode**: not hardcoded loopback, IPv6 hosts bracketed |
|
|
181
|
+
| `NEXUS_API_KEY` | — | bearer token; see below |
|
|
182
|
+
| `NEXUS_TIER` | `metadata_only` | `metadata_only` / `hashed` / `full` |
|
|
183
|
+
| `NEXUS_CONFIG_FILE` | — | JSON file, read only when named; no implicit search path |
|
|
184
|
+
| `NEXUS_HEALTH_INTERVAL_MS` | `60000` | `service_health` window; `0` disables |
|
|
185
|
+
| `NEXUS_SPILL_DIR` | — | durable overflow for batches the collector permanently refused |
|
|
186
|
+
| `NEXUS_QUEUE_CAPACITY` | `10000` | events, then drop-oldest |
|
|
187
|
+
| `NEXUS_BATCH_SIZE` | `500` | |
|
|
188
|
+
| `NEXUS_FLUSH_INTERVAL_MS` / `NEXUS_FLUSH_DEADLINE_MS` | `2000` / `2000` | |
|
|
189
|
+
| `NEXUS_HTTP_TIMEOUT_MS` | `2000` | |
|
|
190
|
+
| `NEXUS_SDK_SINK` | — | append events to a file as NDJSON, for debugging |
|
|
191
|
+
| `NEXUS_APPLICATION` / `NEXUS_REPO` / `NEXUS_COMMIT` / `NEXUS_BRANCH` / `NEXUS_DEPLOYMENT_ID` | auto | provenance; auto-detected from Vercel and GitHub Actions |
|
|
192
|
+
| `NEXUS_DEBUG` | — | `1` writes contained errors to stderr |
|
|
193
|
+
|
|
194
|
+
Events are `POST`ed to `{collector}/v1/events` as `{"events": [...]}`.
|
|
195
|
+
|
|
196
|
+
**The bearer token is withheld from plain HTTP to a non-loopback host.** `https:` anywhere is fine,
|
|
197
|
+
and `http:` to loopback is fine; anything else and the token is left off the request rather than
|
|
198
|
+
sent in cleartext across a network. The request still goes, so you get a diagnosable 401 instead of
|
|
199
|
+
silence.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Privacy tiers
|
|
204
|
+
|
|
205
|
+
Every free-text field carries its shape at every tier — `<name>_chars` and `<name>_fingerprint` are
|
|
206
|
+
content-free, so "the same error as yesterday" stays answerable and a repeated target stays
|
|
207
|
+
groupable without the text leaving the process. What changes per tier is the content beside it.
|
|
208
|
+
|
|
209
|
+
| Tier | On the wire |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `metadata_only` (default) | Shape only. The content key is absent — not blank, not masked. |
|
|
212
|
+
| `hashed` | plus `<name>_preview`: redacted text, truncated. |
|
|
213
|
+
| `full` | plus `<name>`: redacted text, at the field's full limit. |
|
|
214
|
+
|
|
215
|
+
**A tier is a decision about content, never a waiver on credentials.** `full` is not "unscrubbed",
|
|
216
|
+
it is "scrubbed, at greater length": bearer tokens, API keys in nineteen vendor shapes, PEM blocks,
|
|
217
|
+
Luhn-valid card numbers, mod-97-valid IBANs, national IDs, phone numbers and email addresses go
|
|
218
|
+
regardless of tier. Structured values are scrubbed too, which is the case a free-text redactor
|
|
219
|
+
misses — `effect({ headers: { Authorization: 'Bearer …' } })` is masked by key name, and a number
|
|
220
|
+
under a key that is not a recognised measurement is masked below `full`.
|
|
221
|
+
|
|
222
|
+
The redactor is a port of the Python SDK's, and the two are checked against each other over a
|
|
223
|
+
shared corpus (`node scripts/redact-parity.mjs`, 178 cases). One tier setting means the same thing
|
|
224
|
+
in a Node service and a Python one.
|
|
225
|
+
|
|
226
|
+
It is not magic, and the limits are worth knowing. It is a bounded set of anchored patterns plus
|
|
227
|
+
key-name matching, not an entropy scan, because it runs on your request path. `Jane Doe` is not
|
|
228
|
+
recognisable by any regular expression, so a field naming a person still contains their name at
|
|
229
|
+
`full`. The default tier is the primary control; redaction is the second line.
|
|
230
|
+
|
|
231
|
+
**The AI SDK bridge emits tool inputs and outputs on this ladder, and no model text.** Tool payloads
|
|
232
|
+
are structured, which is exactly what the scrubber is for. Model text reaches the ledger through
|
|
233
|
+
[`@swfte/nexus-sdk/otel`](#opentelemetry) instead, as `model_response` and `model_thinking`, where
|
|
234
|
+
it is re-gated by your tier on ingest because upstream redaction is not trusted. The prompt itself
|
|
235
|
+
is emitted by neither path: there is no `prompt` event in the contract for a second producer to
|
|
236
|
+
write against.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## OpenTelemetry
|
|
241
|
+
|
|
242
|
+
If you already run GenAI instrumentation — OpenInference, OpenLLMetry/OpenLIT, or anything emitting
|
|
243
|
+
the OTel GenAI semantic conventions — the bridge turns its spans into nexus events:
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
247
|
+
import { spanProcessor } from '@swfte/nexus-sdk/otel';
|
|
248
|
+
|
|
249
|
+
new NodeTracerProvider({ spanProcessors: [spanProcessor()] }).register();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Same direction as the AI SDK bridge: **you import OpenTelemetry, not us.** `spanProcessor()` returns
|
|
253
|
+
a plain object with the four methods the interface requires, so this package still has no
|
|
254
|
+
dependency and no version range to conflict with yours.
|
|
255
|
+
|
|
256
|
+
Three things it does deliberately:
|
|
257
|
+
|
|
258
|
+
- **An unclassified span is dropped, not guessed at.** A GenAI span whose kind is not modelled is
|
|
259
|
+
counted under `bridge_unclassified` and emitted nowhere — never defaulted into `behavior_trace`,
|
|
260
|
+
which is the class the ledger treats as evidence. A bridge that guesses produces confidently wrong
|
|
261
|
+
events, which is worse than none. `stats().unclassified` is the number to alert on.
|
|
262
|
+
- **Model text is re-gated by your tier on ingest.** Every instrumentation in this space has its own
|
|
263
|
+
content-capture switch and its own idea of what a secret looks like; some have none. Whether text
|
|
264
|
+
leaves your process is your decision, not theirs.
|
|
265
|
+
- **A tool call's arguments contribute a shape, not their values.** `{amount: 4200, note: '…'}`
|
|
266
|
+
becomes field names and types with no value at any depth, because `tool_action.target` is an
|
|
267
|
+
identifier field and a payload must not be standing in front of a gate that assumes one.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Enforcement
|
|
272
|
+
|
|
273
|
+
Every agent-observability tool observes. This one can also refuse:
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
import * as policy from '@swfte/nexus-sdk/policy';
|
|
277
|
+
|
|
278
|
+
policy.install(signedEnvelope); // Ed25519, verified before a rule is read
|
|
279
|
+
|
|
280
|
+
run.action('db.write', 'prod-orders'); // throws Denied if a rule says no
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Importing that module gates `run.action(...)`. **It is the only gated call site**, and it is the
|
|
284
|
+
only one that can be: enforcement needs a decision taken before the effect, which needs a call site
|
|
285
|
+
you own. Tool calls made through the AI SDK seam cannot be gated in-process — see below.
|
|
286
|
+
|
|
287
|
+
A policy is a signed envelope from your control plane. A rule refuses for real only if it carries
|
|
288
|
+
`"enforce": true`; everything else evaluates, records, and lets the call through, so a shadow
|
|
289
|
+
deployment tells you what *would* have been blocked without blocking it.
|
|
290
|
+
|
|
291
|
+
The failure semantics are the part worth reading twice, because they decide what happens on your
|
|
292
|
+
worst day:
|
|
293
|
+
|
|
294
|
+
| Situation | What happens |
|
|
295
|
+
|---|---|
|
|
296
|
+
| No policy installed, or one that will not verify | **Allow**, and raise an integrity alert |
|
|
297
|
+
| Policy verified but stale | `enforce`-marked rules **keep enforcing**; unmarked ones become advice |
|
|
298
|
+
| Evaluation exceeds its latency budget | **Allow**, and record the overrun |
|
|
299
|
+
| A bug inside policy evaluation | **Allow**, and raise an integrity alert |
|
|
300
|
+
| A rule needs a human, and there is no approver | The rule's own `on_timeout`: **deny** if enforce-marked, **allow** if not |
|
|
301
|
+
|
|
302
|
+
**Denying because we could not reach something is never a default.** `failClosed` is the single
|
|
303
|
+
opt-in exception, and it says so in the reason string on every decision it produces — a service
|
|
304
|
+
that starts refusing should be able to tell you why without a support ticket.
|
|
305
|
+
|
|
306
|
+
`Denied` is the only exception this SDK will ever put in your stack trace, and you opted into it
|
|
307
|
+
twice: the rule carried `enforce: true` and the call site did not decline. **With no policy
|
|
308
|
+
installed — the default — nothing here can throw.**
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## What this does not do
|
|
313
|
+
|
|
314
|
+
Named, because a gap that is named is fine and a gap that is silent is not.
|
|
315
|
+
[`PARITY.md`](./PARITY.md) has the full list against the Python SDK.
|
|
316
|
+
|
|
317
|
+
- **No module-hook auto-instrumentation.** No `--import @swfte/nexus-sdk/register`. It cannot serve
|
|
318
|
+
bundled applications, and shipping a half-working version of it produces exactly the empty
|
|
319
|
+
dashboard this SDK is supposed to make impossible. The spike's implementation is still in the
|
|
320
|
+
repository under `src/hooks/`, excluded from the published package.
|
|
321
|
+
- **No provider adapters.** No direct instrumentation of `@anthropic-ai/sdk` or `openai`. If you
|
|
322
|
+
call those libraries directly rather than through the AI SDK, use `run.usage(...)`.
|
|
323
|
+
- **No enforcement through the AI SDK.** The seam is observe-only *by its type signature* — the
|
|
324
|
+
callback returns `void`, there is no channel to refuse on, and a throw from
|
|
325
|
+
`onToolExecutionStart` neither stops the tool nor reaches your code. Measured, not assumed. In
|
|
326
|
+
Node, enforcement means the explicit API, where you own the call site.
|
|
327
|
+
- **Enforcement reaches exactly one call site.** `@swfte/nexus-sdk/policy` gates
|
|
328
|
+
`run.action(...)`, and nothing else. **An application using the AI SDK seam cannot be gated
|
|
329
|
+
in-process at all**, now or later: that seam's callbacks return `void`, so there is no channel to
|
|
330
|
+
refuse on, and a throw from `onToolExecutionStart` neither stops the tool nor reaches your code.
|
|
331
|
+
Enforcement in Node means the explicit API where you own the call site, a gateway in front of your
|
|
332
|
+
provider, or nothing. Worth knowing before you design around it.
|
|
333
|
+
- **No human-approval channel.** A `require_approval` rule resolves to its own `on_timeout` — deny
|
|
334
|
+
if the rule is enforce-marked, allow if not — rather than waiting for a person. Node cannot park
|
|
335
|
+
its event loop, and a synchronous wait would freeze the process.
|
|
336
|
+
- **The OTel bridge does not deduplicate retries.** `@swfte/nexus-sdk/otel` reads spans from
|
|
337
|
+
whatever GenAI instrumentation you run and emits `token_usage`, `tool_action`,
|
|
338
|
+
`model_response` and `model_thinking`. What it does not port is Python's multi-span logical-call
|
|
339
|
+
join, so against an instrumentation that emits one span per HTTP attempt, token counts sum across
|
|
340
|
+
retries instead of being deduplicated.
|
|
341
|
+
- **No `nexus-run` launcher.** There is no Node equivalent of `PYTHONPATH`-based bootstrapping that
|
|
342
|
+
survives bundling.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Why no dependencies
|
|
347
|
+
|
|
348
|
+
The core declares nothing. Not an HTTP client, not a UUID library.
|
|
349
|
+
|
|
350
|
+
This is a product decision, not asceticism. An observability SDK is installed into a dependency
|
|
351
|
+
graph somebody else has already resolved, and every requirement we add is a version range that can
|
|
352
|
+
conflict with theirs — a conflict at install time is where the adoption conversation ends, before
|
|
353
|
+
any of this code runs. `scripts/npm-guard.cjs` and a test both fail the build if `dependencies`,
|
|
354
|
+
`peerDependencies` or `optionalDependencies` stops being empty.
|
|
355
|
+
|
|
356
|
+
The `/ai` entry point is the shape this forces and it is a better shape anyway: we never import
|
|
357
|
+
`ai`, you hand us `registerTelemetry`.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Development
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
npm test # 339 tests, node --test, no third-party framework
|
|
365
|
+
npm run typecheck # tsc --strict against the public declarations
|
|
366
|
+
npm run guard # what `npm publish` would ship, checked against an allowlist
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Two checks need a network and are not part of `npm test`:
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
npm run probe:ai # the bridge against the real `ai` package, end to end
|
|
373
|
+
npm run parity # redactor, rate card and policy envelope, diffed against Python
|
|
374
|
+
bash scripts/consumer-check.sh # install the tarball; run strict tsc against it as a consumer
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`test/contract.test.mjs` validates every emitted event against `contract/events.v1.json` from
|
|
378
|
+
`nexus-devtools`. It looks for it at `../nexus-devtools/contract/events.v1.json`, overridable with
|
|
379
|
+
`NEXUS_CONTRACT`, and skips loudly rather than silently when it is absent.
|
|
380
|
+
|
|
381
|
+
### Layout
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
src/core.cjs the SDK: config, context, queue, transport, run/action, contract
|
|
385
|
+
src/index.js .cjs ESM and CJS entries over the one core (dual-package hazard)
|
|
386
|
+
src/ai.cjs .js the Vercel AI SDK bridge
|
|
387
|
+
src/provenance.cjs where a commit and a repo came from, and how confident we are
|
|
388
|
+
src/hooks/ the spike's module hooks — NOT shipped, kept for the record
|
|
389
|
+
fixtures/ stand-in provider, probe applications, the real-library probes
|
|
390
|
+
bundle/ esbuild build + the start-up measurement harness
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Nexus, beyond this package
|
|
396
|
+
|
|
397
|
+
This SDK is one attach point of three. All three write the same events to the same ledger, so a run
|
|
398
|
+
in production and a run on a laptop are the same shape in the same tables.
|
|
399
|
+
|
|
400
|
+
| | Install | What it attaches to |
|
|
401
|
+
|---|---|---|
|
|
402
|
+
| **Terminal wrapper** | `npm i -g @swfte/nexus` · `pip install swfte-nexus` | coding agents in a developer's terminal — Claude Code, Codex |
|
|
403
|
+
| **Node SDK** — this package | `npm i @swfte/nexus-sdk` | your own Node services |
|
|
404
|
+
| **[Python SDK](https://github.com/SwfteAI/nexus-sdk)** | `pip install swfte-nexus-sdk` | your own Python services |
|
|
405
|
+
|
|
406
|
+
- **Product, pricing and docs:** [www.swfte.com](https://www.swfte.com)
|
|
407
|
+
- **Self-hosting, procurement, security review, pilots:** [sales@swfte.com](mailto:sales@swfte.com)
|
|
408
|
+
- **Report a vulnerability:** [`SECURITY.md`](./SECURITY.md)
|
|
409
|
+
|
|
410
|
+
## Licence
|
|
411
|
+
|
|
412
|
+
Apache-2.0. See [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE).
|
|
413
|
+
|
|
414
|
+
Built by **[Swfte AI](https://www.swfte.com)**.
|