@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 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
+ [![npm](https://img.shields.io/npm/v/@swfte/nexus-sdk.svg)](https://www.npmjs.com/package/@swfte/nexus-sdk)
6
+ [![Node](https://img.shields.io/node/v/@swfte/nexus-sdk.svg)](https://www.npmjs.com/package/@swfte/nexus-sdk)
7
+ [![Licence](https://img.shields.io/badge/licence-Apache--2.0-blue.svg)](LICENSE)
8
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#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)**.