nexus-ai-pro 2.4.0 → 2.5.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/API_STABILITY.md +85 -0
- package/CHANGELOG.md +147 -1
- package/README.md +8 -3
- package/SECURITY.md +78 -9
- package/dist/context-hub/hub.d.ts +53 -0
- package/dist/context-hub/hub.js +108 -3
- package/dist/context-hub/index.d.ts +2 -1
- package/dist/context-hub/index.js +2 -1
- package/dist/context-hub/signing.d.ts +95 -0
- package/dist/context-hub/signing.js +88 -0
- package/dist/doctor/index.d.ts +4 -2
- package/dist/doctor/index.js +29 -0
- package/dist/evaluate/threads.d.ts +187 -0
- package/dist/evaluate/threads.js +187 -0
- package/dist/operations/adapters.d.ts +2 -1
- package/dist/operations/runner.js +2 -0
- package/dist/operations/stats.js +11 -4
- package/dist/postgres/index.d.ts +2 -1
- package/dist/postgres/index.js +4 -0
- package/dist/postgres/operations.js +11 -4
- package/dist/postgres/store.d.ts +7 -0
- package/dist/postgres/store.js +21 -0
- package/dist/postgres/tenancy.d.ts +43 -0
- package/dist/postgres/tenancy.js +71 -0
- package/dist/server/deployments.d.ts +127 -12
- package/dist/server/deployments.js +402 -37
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/metrics.js +7 -0
- package/dist/server/runs.d.ts +9 -1
- package/dist/server/runs.js +46 -7
- package/dist/server/server.js +4 -1
- package/dist/server/state.d.ts +12 -2
- package/dist/server/state.js +15 -0
- package/dist/sqlite/operations.js +15 -4
- package/dist/sqlite/store.d.ts +7 -0
- package/dist/sqlite/store.js +28 -0
- package/dist/store/memory.d.ts +8 -0
- package/dist/store/memory.js +12 -1
- package/dist/store/redis.d.ts +13 -0
- package/dist/store/redis.js +38 -0
- package/dist/tracing/instrument.js +1 -0
- package/dist/tracing/tracer.d.ts +4 -0
- package/dist/tracing/tracer.js +6 -0
- package/dist/types/operations.d.ts +7 -0
- package/dist/types/server.d.ts +61 -2
- package/dist/types/store.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/context-hub/hub.d.ts +53 -0
- package/dist-cjs/context-hub/hub.js +110 -4
- package/dist-cjs/context-hub/index.d.ts +2 -1
- package/dist-cjs/context-hub/index.js +7 -1
- package/dist-cjs/context-hub/signing.d.ts +95 -0
- package/dist-cjs/context-hub/signing.js +96 -0
- package/dist-cjs/doctor/index.d.ts +4 -2
- package/dist-cjs/doctor/index.js +29 -0
- package/dist-cjs/evaluate/threads.d.ts +187 -0
- package/dist-cjs/evaluate/threads.js +198 -0
- package/dist-cjs/operations/adapters.d.ts +2 -1
- package/dist-cjs/operations/runner.js +2 -0
- package/dist-cjs/operations/stats.js +11 -4
- package/dist-cjs/postgres/index.d.ts +2 -1
- package/dist-cjs/postgres/index.js +8 -1
- package/dist-cjs/postgres/operations.js +11 -4
- package/dist-cjs/postgres/store.d.ts +7 -0
- package/dist-cjs/postgres/store.js +21 -0
- package/dist-cjs/postgres/tenancy.d.ts +43 -0
- package/dist-cjs/postgres/tenancy.js +110 -0
- package/dist-cjs/server/deployments.d.ts +127 -12
- package/dist-cjs/server/deployments.js +402 -37
- package/dist-cjs/server/metrics.d.ts +2 -0
- package/dist-cjs/server/metrics.js +8 -0
- package/dist-cjs/server/runs.d.ts +9 -1
- package/dist-cjs/server/runs.js +45 -6
- package/dist-cjs/server/server.js +4 -1
- package/dist-cjs/server/state.d.ts +12 -2
- package/dist-cjs/server/state.js +15 -0
- package/dist-cjs/sqlite/operations.js +15 -4
- package/dist-cjs/sqlite/store.d.ts +7 -0
- package/dist-cjs/sqlite/store.js +28 -0
- package/dist-cjs/store/memory.d.ts +8 -0
- package/dist-cjs/store/memory.js +12 -1
- package/dist-cjs/store/redis.d.ts +13 -0
- package/dist-cjs/store/redis.js +38 -0
- package/dist-cjs/tracing/instrument.js +1 -0
- package/dist-cjs/tracing/tracer.d.ts +4 -0
- package/dist-cjs/tracing/tracer.js +6 -0
- package/dist-cjs/types/operations.d.ts +7 -0
- package/dist-cjs/types/server.d.ts +61 -2
- package/dist-cjs/types/store.d.ts +9 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +27 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,91 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.5 stage (2.5.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.5 adds is stable from the start and additive.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `putIfVersion()`, optional on `Store` and `ServerStateStore`, and its implementations;
|
|
50
|
+
`Deployments.atomicChanges`; `ScalingSnapshot.atomicChanges`;
|
|
51
|
+
- `nexus-ai-pro/postgres/tenancy`: `PostgresTenantUsage`, `PostgresTenantUsageOptions`,
|
|
52
|
+
`tenantUsageMigration()`, and `tenantUsageMigrations()`; the `tenancy` member of
|
|
53
|
+
`PostgresAdapter`;
|
|
54
|
+
- `OperationStoreFilter.kinds`, `OperationStoreStats.queuedByKind`, `RevisionRequest.queued`,
|
|
55
|
+
`RevisionChoice.local`, `ScalingSnapshot.queuedByRevision`, and the
|
|
56
|
+
`nexus_server_runs_queued_by_revision` metric.
|
|
57
|
+
|
|
58
|
+
- signing on `nexus-ai-pro/context-hub`: `ContextHubOptions.signer`, `keyring`, and `requireSignature`;
|
|
59
|
+
`ContextHub.verify()` and `ContextVerification`; `ContextBundle.signatures`; `ContextSignatureError`;
|
|
60
|
+
`generateContextKey()`, `ed25519Signer()`, `ed25519Keyring()`, `contextDigest()`, and the signing
|
|
61
|
+
types. The signed digest's format is `nexus-context-signature`, format version 1, and a later release
|
|
62
|
+
verifies it;
|
|
63
|
+
- `metadata.context` on a traced model call;
|
|
64
|
+
- on `nexus-ai-pro/server/deployments`, experimental with the rest of it: `BucketStrategy`,
|
|
65
|
+
`DeploymentRecord.bucketStrategy`, `RevisionedAssistantOptions.bucketStrategy`, the `bucketing`
|
|
66
|
+
change and `Deployments.bucketing()`, the strategy argument of `bucket()`; `recordRun()`,
|
|
67
|
+
`rollups()`, `flushRollups()`, `stats({ deployment })`, `DeploymentRollup`, `RevisionRollup`, and
|
|
68
|
+
`DeploymentsOptions.rollupMs`; `CanaryGuardOptions.from`. `ServerDeployments` gains an optional
|
|
69
|
+
`recordRun()`, and `RunManagerOptions.deployments`. The positions `fnv1a-v1` and `hash-v2` give a key
|
|
70
|
+
never change;
|
|
71
|
+
- the doctor's `settings` check; `RedisOperationStore.indexed`; `Tracer.store` and
|
|
72
|
+
`Tracer.incremental`;
|
|
73
|
+
- `nexus-ai-pro/evaluate/threads`, experimental: `simulatedUser()`, `scriptedUser()`,
|
|
74
|
+
`conversationTarget()`, `graphThreadAgent()`, `formatThread()`, `goalReached()`,
|
|
75
|
+
`goalCompletion()`, `turnCount()`, `turnScores()`, and their types. The score keys `goal_reached`,
|
|
76
|
+
`goal_completion`, `turns`, and `turn_score` are stable once the entry point graduates.
|
|
77
|
+
|
|
78
|
+
The studio now refuses, as the compatibility rules allow for security: a change whose body is not
|
|
79
|
+
`application/json`, with `415`; a body that is not a JSON object, with `400`; a body past
|
|
80
|
+
`maxBodyBytes`, with `413`; and a list `limit`, `hours`, or `days` past its ceiling, with `400`. With
|
|
81
|
+
accounts, only a bearer `Authorization` header stands in for the page token. The agent server refuses
|
|
82
|
+
an assistant or revision named after a built-in property, such as `constructor`. Also stable from
|
|
83
|
+
2.5: `StudioOptions.maxBodyBytes`, `Studio.routes` and `StudioRoute`, and
|
|
84
|
+
`MemoryStudioJournalOptions.maxComments`.
|
|
85
|
+
|
|
86
|
+
### Graduation
|
|
87
|
+
|
|
88
|
+
Two more surfaces leave experimental status in 2.5, by the same checklist as in 2.4. Every test named
|
|
89
|
+
here runs in CI.
|
|
90
|
+
|
|
91
|
+
**Deployments** (`nexus-ai-pro/server/deployments`):
|
|
92
|
+
|
|
93
|
+
| Requirement | Evidence |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| Conformance | Atomic changes on the memory server store and the memory, Redis, Postgres, and SQLite stores: `tests/atomic-deployments.test.ts`; routing, canaries, and guards: `tests/deployments.test.ts`, `tests/fair-canaries.test.ts`; revision pools: `tests/revision-pools.test.ts` |
|
|
96
|
+
| Crash recovery | A change is one compare-and-set write, so a crash leaves the old record or the new one; a drained replica hands its runs to another: `tests/deployments.test.ts`. A replica that dies loses at most its last `rollupMs` of counts, which the guide states |
|
|
97
|
+
| Concurrency | Ten replicas making mixed changes at once land in one linear history on every store: `tests/atomic-deployments.test.ts` |
|
|
98
|
+
| Load | `deployments.route-canary` and `deployments.bucket` in `surface-budget.json` |
|
|
99
|
+
| Tenant isolation | n/a: a deployment is an assistant's configuration, not a tenant's data. The runs it routes are operations, which are isolated: `tests/tenant-isolation.test.ts` |
|
|
100
|
+
| Security | [Review](./SECURITY.md#deployments): four findings fixed, two documented limitations |
|
|
101
|
+
| Upgrade | A 2.3 replica's deployment routes the same here, and every thread keeps its bucket: `tests/upgrade-previous-minor.test.ts` |
|
|
102
|
+
| Soak | 50,000 route, admit, and release cycles, 500 replica lifecycles, and 20,000 runs counted and judged from rollups, with no timer left running |
|
|
103
|
+
|
|
104
|
+
**The studio and its accounts** (`nexus-ai-pro-studio`):
|
|
105
|
+
|
|
106
|
+
| Requirement | Evidence |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| Conformance | Every view over its sources, with and without accounts: `studio/test/studio.test.ts`, `studio/test/team.test.ts`, `studio/test/deployments.test.ts` |
|
|
109
|
+
| Crash recovery | n/a: the studio keeps no state but its journal, which appends one line per entry and skips a line a crash cut short: `studio/test/security.test.ts` |
|
|
110
|
+
| Concurrency | n/a: every change goes through its source's own API, where races are settled: prompt and bundle labels by compare-and-set, deployments by `putIfVersion()` |
|
|
111
|
+
| Load | 500 signed-in reads within a budget relative to the machine: `studio/test/security.test.ts` |
|
|
112
|
+
| Tenant isolation | A studio over `tenantScope()` views lists one tenant's runs, and another tenant's trace is not found: `studio/test/security.test.ts` |
|
|
113
|
+
| Security | [Review](./SECURITY.md#the-studio-and-its-accounts): ten findings, all fixed; every route fuzzed by every role and with hostile input: `studio/test/security.test.ts` |
|
|
114
|
+
| Upgrade | The journal reads what 2.4 wrote, and accounts and configuration load unchanged: `studio/test/security.test.ts`, `studio/test/team.test.ts` |
|
|
115
|
+
| Soak | 2,000 sign-in, read, and change cycles with flat memory, and 200 servers started and closed with no handle left open: `studio/test/security.test.ts` |
|
|
116
|
+
|
|
117
|
+
**Still experimental**, each with what remains:
|
|
118
|
+
- **Images.** Recorded live conformance must pass on all three backends. The scheduled live workflow
|
|
119
|
+
records it wherever a backend's credential is set, and none has run with all three yet.
|
|
120
|
+
- **The Qdrant, Redis, Pinecone, Weaviate, and Chroma vector stores.** The contract must pass against
|
|
121
|
+
the real servers. The scheduled live workflow runs it against each one, Pinecone when its secret is
|
|
122
|
+
set, and its evidence is published when it has passed against all five.
|
|
123
|
+
|
|
124
|
+
A queued run's kind now names its revision, as `assistant:<id>@<revision>`. Code that reads the
|
|
125
|
+
server's runs by the `assistant:` prefix, as the server itself does, is unaffected.
|
|
126
|
+
|
|
127
|
+
A state store without `putIfVersion()` keeps its earlier behavior.
|
|
128
|
+
|
|
44
129
|
## 2.4 stage (2.4.0)
|
|
45
130
|
|
|
46
131
|
Everything 2.4 adds is stable from the start and additive. These security fixes tighten behavior,
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,151 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.5.0] - 2026-10-09
|
|
8
|
+
|
|
9
|
+
Graduation finished, and conversations evaluated.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Deployment changes that are atomic across replicas.**
|
|
14
|
+
- The long-term `Store` contract and the server's `ServerStateStore` gain an optional
|
|
15
|
+
`putIfVersion()`. It writes a record only when the stored one's `version` is still the version
|
|
16
|
+
the change was decided on, in one atomic step. The memory, Postgres, and SQLite stores, the server's
|
|
17
|
+
memory store, and the Redis store (when its client has `eval`) have it, and `fromStore()` passes
|
|
18
|
+
it on.
|
|
19
|
+
- A deployment change uses it. A replica that loses the race decides again on the winner's record,
|
|
20
|
+
so two changes made at once both land, in order. A change made on an `expectedVersion` is
|
|
21
|
+
refused with `DEPLOYMENT_CONFLICT` instead. Two replicas starting at once seed one record.
|
|
22
|
+
- `Deployments.atomicChanges` and `/scaling`'s `atomicChanges` say whether the state store
|
|
23
|
+
offers this.
|
|
24
|
+
- **Tenant usage in Postgres**, on the new `nexus-ai-pro/postgres/tenancy`: `PostgresTenantUsage`, so
|
|
25
|
+
every replica enforces one tenant limit without Redis. The `tenancy` adapter joins
|
|
26
|
+
`postgresMigrations()` and `nexus db migrate`.
|
|
27
|
+
- **Revision worker pools.** A worker image carries some revisions of an assistant, and claims only
|
|
28
|
+
the runs routed to them, so a new revision ships in a new image beside the old one.
|
|
29
|
+
- A queued run's kind names its revision, as `assistant:<id>@<revision>`, and a worker claims with
|
|
30
|
+
the kinds of the revisions it registered.
|
|
31
|
+
- A run that will wait in the queue is routed over the revisions any fresh worker reports
|
|
32
|
+
(`RevisionRequest.queued`), so the API replicas send traffic to a revision only a new image
|
|
33
|
+
carries. `RevisionChoice.local` says whether the answering replica has it.
|
|
34
|
+
- `/scaling` reports `queuedByRevision`, and `/metrics` `nexus_server_runs_queued_by_revision`, so an
|
|
35
|
+
autoscaler starts the pool a waiting revision needs.
|
|
36
|
+
- `OperationStoreFilter.kinds` filters by exact kinds on every operation store, and
|
|
37
|
+
`OperationStoreStats.queuedByKind` counts queued records per kind. `claimQueued()` checks each
|
|
38
|
+
candidate against the filter itself, for a store that ignores one.
|
|
39
|
+
- **Signed context bundles.**
|
|
40
|
+
- A hub with a `signer` signs every new version with Ed25519, through Web Crypto, so on any
|
|
41
|
+
runtime. The signature covers the bundle's content and its pinned prompts' content, under the
|
|
42
|
+
full SHA-256 that `contextDigest()` computes, not the 12-digit version.
|
|
43
|
+
- A hub with a `keyring` checks signatures with `verify()`. With `requireSignature`, it refuses a
|
|
44
|
+
bundle without a trusted signature, with a `ContextSignatureError`, on `import()` and whenever
|
|
45
|
+
`resolve()` or `renderPrompt()` serves one.
|
|
46
|
+
- An import keeps the signatures that claim its content, and adds the importing hub's. That is how
|
|
47
|
+
keys rotate.
|
|
48
|
+
- `generateContextKey()`, `ed25519Signer()`, and `ed25519Keyring()` make and load keys, as JSON Web
|
|
49
|
+
Keys or Web Crypto keys.
|
|
50
|
+
- **Canaries that stay fair.**
|
|
51
|
+
- A deployment records a `bucketStrategy`. `hash-v2` mixes the thread id's hash with
|
|
52
|
+
MurmurHash3's finalizer, so sequential ids such as `user-1` to `user-500` split as evenly as random
|
|
53
|
+
ones. `fnv1a-v1` stays the default, and every deployment recorded before keeps it, so no thread
|
|
54
|
+
moves through an upgrade. `Deployments.assistant()` takes `bucketStrategy`; `bucketing()` and a
|
|
55
|
+
`bucketing` change switch a recorded deployment. `bucket()` takes the strategy.
|
|
56
|
+
- Every replica counts the runs it finishes, per revision, in a rollup under the deployment version
|
|
57
|
+
they were routed with: counts, latency in buckets 5% apart, and cost. `stats()` with `deployment`
|
|
58
|
+
and `rollups()` sum them in one read, and `GET /deployments/:assistant` reports them.
|
|
59
|
+
- `watchCanaries({ from: 'rollups' })` judges a canary on every run since the split last changed,
|
|
60
|
+
instead of the latest 2,000 run records.
|
|
61
|
+
- **A doctor that knows the opt-in settings.** `nexus doctor` and `diagnose()` warn, in a new
|
|
62
|
+
`settings` check, when a `RedisOperationStore` runs without `index: true`, and when a `Tracer`
|
|
63
|
+
writes to a persistent store without `incremental`. Each warning names the setting that fixes it.
|
|
64
|
+
Both stay opt-in through 2.x. `RedisOperationStore.indexed`, `Tracer.store`, and
|
|
65
|
+
`Tracer.incremental` report the settings.
|
|
66
|
+
- **Graduation.** Deployments and the studio leave experimental status, each with its checklist and
|
|
67
|
+
evidence in `API_STABILITY.md` and a threat review in `SECURITY.md`.
|
|
68
|
+
- **The studio's routes, listed.** `Studio.routes` gives every route's method, path, role, and audit
|
|
69
|
+
action. `StudioOptions.maxBodyBytes` bounds a request body, and
|
|
70
|
+
`MemoryStudioJournalOptions.maxComments` bounds the comments kept in memory.
|
|
71
|
+
- **Whole conversations, evaluated** (`nexus-ai-pro/evaluate/threads`).
|
|
72
|
+
- `simulatedUser()` plays a user with a goal and a persona, driven by a model, and says when the goal
|
|
73
|
+
was met or when it gives up. `scriptedUser()` follows a script, for deterministic tests.
|
|
74
|
+
- `conversationTarget()` is an `evaluate()` target that holds a conversation per example and returns
|
|
75
|
+
the `Thread`: every turn, how long each answer took, the messages the agent added, and how it
|
|
76
|
+
ended. `graphThreadAgent()` drives an agent from `createAgent()`, which keeps its own tool calls
|
|
77
|
+
from turn to turn.
|
|
78
|
+
- Thread evaluators: `goalReached()`, `goalCompletion()` with a judge, `turnCount()`, and
|
|
79
|
+
`turnScores()`, which scores every turn with an evaluator of single answers and names the turn
|
|
80
|
+
where the conversation went wrong.
|
|
81
|
+
- **Context on every trace.** A traced model call records `metadata.context` beside
|
|
82
|
+
`metadata.prompt`, so a trace query or an insight narrows by bundle version.
|
|
83
|
+
|
|
84
|
+
### Security
|
|
85
|
+
|
|
86
|
+
- **The studio**, from its threat review:
|
|
87
|
+
- A request body is refused with `413` past `maxBodyBytes`, 1 MiB by default, as it arrives and
|
|
88
|
+
before anyone is signed in. Before, a body was read whole into memory first.
|
|
89
|
+
- With accounts, only a bearer `Authorization` header stands in for the page token. Before, any
|
|
90
|
+
`Authorization` header did, and a browser adds Basic credentials to another site's request by
|
|
91
|
+
itself, so a sign-in proxy that uses Basic authentication let another site make changes.
|
|
92
|
+
- A change's body must be `application/json` (`415` otherwise) and a JSON object (`400`
|
|
93
|
+
otherwise).
|
|
94
|
+
- A change refused for want of the page token is recorded in the audit log.
|
|
95
|
+
- Graphs and review queues are looked up among the application's own entries, so a name such as
|
|
96
|
+
`constructor` no longer reaches the prototype.
|
|
97
|
+
- Malformed percent-encoding in a path is a `400`, not a server error.
|
|
98
|
+
- `limit`, `hours`, and `days` have ceilings: 1,000 items, 10,000 audit entries, 90 days of
|
|
99
|
+
issues, and 366 days of costs.
|
|
100
|
+
- The memory journal keeps at most 10,000 comments.
|
|
101
|
+
- The server no longer sends an internal error's message, and logs it instead.
|
|
102
|
+
- Only `http` and `https` pull-request links are rendered.
|
|
103
|
+
- **The agent server** refuses an assistant or revision named after a built-in property, such as
|
|
104
|
+
`constructor`, with `400`. Before, such a run was accepted and failed when it ran.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- A worker that lacked the revision a run was routed to ran the assistant's live revision instead,
|
|
109
|
+
and the run still recorded the revision it was routed to. A worker now claims only runs of revisions
|
|
110
|
+
it carries.
|
|
111
|
+
|
|
112
|
+
### Proof
|
|
113
|
+
|
|
114
|
+
- The studio's threat review is a test: every route is called by every role and refuses the roles
|
|
115
|
+
below its own, recording each refusal; over 2,000 requests with hostile paths, queries, and bodies
|
|
116
|
+
all answer below `500`, and no prototype is polluted. 2,000 sign-in, read, and change cycles leave
|
|
117
|
+
the heap flat, and 200 servers start and close with no handle left open.
|
|
118
|
+
- The doctor's seeded deployment warns about a Redis operation store without its index and a tracer
|
|
119
|
+
writing to a persistent store at the end of each trace, and the same deployment with both settings
|
|
120
|
+
turned on passes.
|
|
121
|
+
- Ten replicas make mixed changes to one deployment at once (canaries, splits, promotions,
|
|
122
|
+
rollbacks, and a bucketing change), held until all ten arrive, on the memory server store and the
|
|
123
|
+
memory, Redis, Postgres, and SQLite stores. Every change lands, each with a version of its own, and
|
|
124
|
+
the history is one linear sequence.
|
|
125
|
+
- Over 48 sets of 500 sequential thread ids, `hash-v2` places a canary's share within 2.3 standard
|
|
126
|
+
deviations of its weight, as random placement does; `fnv1a-v1` strays by up to 9.9. Over 10,000
|
|
127
|
+
sequential ids, `hash-v2` is within half a point at every weight.
|
|
128
|
+
- A canary that fails 0.8% of the time against 0.5% is held by a guard over the latest 2,000 run
|
|
129
|
+
records, and rolled back by a guard over rollups, which counts all 100,000 runs from three replicas
|
|
130
|
+
in one read.
|
|
131
|
+
- A simulated user holds 50 conversations with each of two versions of a support agent built with
|
|
132
|
+
`createAgent()`. The thread evaluators score every conversation, and `compareExperiments()` finds
|
|
133
|
+
the second version better on goals met, the judge's verdict, turns taken, and per-turn scores, with
|
|
134
|
+
no regression; the reverse comparison regresses. A conversation that failed names the turn where it
|
|
135
|
+
went wrong.
|
|
136
|
+
- A signed bundle imports into a hub that requires signatures. It is refused there when it is unsigned,
|
|
137
|
+
or edited after export even with its version recomputed. One edited where it is stored is refused
|
|
138
|
+
when it is served. A re-import through a hub with a new key keeps both signatures, so the bundle
|
|
139
|
+
stays trusted once the old key is retired. Every traced model call rendered from it names its
|
|
140
|
+
context and prompt versions.
|
|
141
|
+
- Two worker images drain one queue, one carrying v1 and the other v1 and v2, with an API replica
|
|
142
|
+
that carries only v1. Under a 50% canary, every run executes on the revision it was routed to, and
|
|
143
|
+
only the image with v2 runs v2. A run for a pool scaled to zero waits, is counted per revision on
|
|
144
|
+
`/scaling` and `/metrics`, and runs when that pool returns.
|
|
145
|
+
- Two replicas changing one deployment in the same instant both land, on the memory, Redis,
|
|
146
|
+
Postgres, and SQLite stores. The race test holds both writers until both arrive, and the same race
|
|
147
|
+
on a store without `putIfVersion()` loses a change. Of two changes made on one expected version,
|
|
148
|
+
exactly one is refused.
|
|
149
|
+
- Ten workers spending one tenant budget lose nothing, and never over-grant a slot, on Postgres as
|
|
150
|
+
on memory and Redis.
|
|
151
|
+
|
|
7
152
|
## [2.4.0] - 2026-10-08
|
|
8
153
|
|
|
9
154
|
Graduation and interoperability.
|
|
@@ -1959,7 +2104,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
1959
2104
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
1960
2105
|
- CLI and audit findings no longer disclose detected secret values.
|
|
1961
2106
|
|
|
1962
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
2107
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.5.0...HEAD
|
|
2108
|
+
[2.5.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.4.0...v2.5.0
|
|
1963
2109
|
[2.4.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.3.0...v2.4.0
|
|
1964
2110
|
[2.3.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.2.0...v2.3.0
|
|
1965
2111
|
[2.2.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.1.0...v2.2.0
|
package/README.md
CHANGED
|
@@ -266,12 +266,17 @@ Shipped so far:
|
|
|
266
266
|
- 2.4 graduation and interoperability: SQLite, loaders, retrievers, the MCP registry, the context
|
|
267
267
|
hub, insights, and tenant limits out of experimental with published evidence; canaries judged with
|
|
268
268
|
confidence and on quality; an adapter kit; AG-UI, A2A, and ACP; and a portable kernel, with the
|
|
269
|
-
client, providers, graphs, and agents running on Deno, Bun, edge runtimes, and browsers
|
|
269
|
+
client, providers, graphs, and agents running on Deno, Bun, edge runtimes, and browsers;
|
|
270
|
+
- 2.5 graduation finished, and conversations evaluated: deployments that change atomically across
|
|
271
|
+
replicas, revision worker pools, fair canaries judged on every run, and the studio out of
|
|
272
|
+
experimental after a threat review; Postgres tenant usage; signed context named on every trace;
|
|
273
|
+
simulated users and thread evaluators; and a doctor that knows the opt-in settings.
|
|
270
274
|
|
|
271
275
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
272
276
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
Next, 2.6 completes the agent protocols: tool progress on every stream, AG-UI deltas and frontend
|
|
278
|
+
tools, A2A tasks and ACP sessions that survive a restart, and a browser in CI. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
|
|
279
|
+
releases after it.
|
|
275
280
|
|
|
276
281
|
## Before Production
|
|
277
282
|
|
package/SECURITY.md
CHANGED
|
@@ -52,7 +52,7 @@ what it could do with that input. Each review is published here with its finding
|
|
|
52
52
|
before the surface graduates. A finding is closed in one of two ways: by a fix with a test that
|
|
53
53
|
names the attack, or by a documented limitation that the surface's guide states.
|
|
54
54
|
|
|
55
|
-
These reviews were done for 2.4.
|
|
55
|
+
These reviews were done for 2.4, and for 2.5 where a review says so.
|
|
56
56
|
|
|
57
57
|
### SQLite adapters
|
|
58
58
|
|
|
@@ -168,7 +168,18 @@ server trusts by itself.
|
|
|
168
168
|
|
|
169
169
|
Findings: none.
|
|
170
170
|
|
|
171
|
-
### Deployments
|
|
171
|
+
### Deployments
|
|
172
|
+
|
|
173
|
+
Reviewed for 2.4, and again for 2.5, when it graduated.
|
|
174
|
+
|
|
175
|
+
Trust boundary: a deployment is changed by an admin, a canary guard, or the studio's admins; a run
|
|
176
|
+
names its assistant, and optionally a revision, in a request from any caller with the write scope.
|
|
177
|
+
Replicas, guards, and the studio share the server's state store, which they trust.
|
|
178
|
+
- Every change is decided on the version it read and written by compare-and-set, on every included
|
|
179
|
+
state store, so a stale change never overwrites a newer one.
|
|
180
|
+
- A guard acts on the version it judged, so guards on every replica never undo each other.
|
|
181
|
+
- A replica writes rollups only to its own record. Counts are as trustworthy as the state store, as
|
|
182
|
+
run records already are.
|
|
172
183
|
|
|
173
184
|
Findings:
|
|
174
185
|
- **Fixed in 2.4: one caller's ratings could decide a canary.** A canary was judged on feedback
|
|
@@ -177,10 +188,68 @@ Findings:
|
|
|
177
188
|
key, as the mean of its ratings.
|
|
178
189
|
- **Fixed in 2.4: two changes at once on one replica could lose one**, such as a guard's rollback
|
|
179
190
|
and an operator's promotion. Changes to one deployment now apply in order.
|
|
180
|
-
- **
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
- **Fixed in 2.5: two replicas changing one deployment in the same instant could lose one change.**
|
|
192
|
+
The state store gained `putIfVersion()`, and a replica that loses the race decides again on the
|
|
193
|
+
winner's record. Proof: ten replicas held until all arrive make mixed changes on every store, and
|
|
194
|
+
every change lands in one linear history.
|
|
195
|
+
- **Fixed in 2.5: a name from a request could reach an object's prototype.** A run for the assistant
|
|
196
|
+
`constructor`, or naming the revision `constructor`, was accepted and failed when it ran. Only an
|
|
197
|
+
application's own assistants and revisions are looked up now, and both are refused with `400`.
|
|
198
|
+
- **Documented limitation: `fnv1a-v1` spreads sequential ids unevenly.** It stays the default, since
|
|
199
|
+
its positions are part of the stability promise. `hash-v2` spreads them evenly, opt-in per
|
|
200
|
+
deployment.
|
|
201
|
+
- **Documented limitation: a replica that dies loses the runs it counted in its last `rollupMs`.**
|
|
202
|
+
Rollups are statistics; the run records stay whole.
|
|
203
|
+
|
|
204
|
+
### The studio and its accounts
|
|
205
|
+
|
|
206
|
+
Reviewed for 2.5, when it graduated.
|
|
207
|
+
|
|
208
|
+
Trust boundary: anyone who can reach the port, before signing in; a signed-in person, limited by
|
|
209
|
+
their role; a page on another site acting through a signed-in person's browser; and the data the
|
|
210
|
+
studio shows, which traces, models, and people wrote.
|
|
211
|
+
- The `Host` header must be a loopback name or one in `allowedHosts`, which closes DNS rebinding.
|
|
212
|
+
- Tokens are compared in constant time. A token in the URL is moved into an HTTP-only, `SameSite=Strict`
|
|
213
|
+
cookie, `Secure` over HTTPS, and taken out of the address bar. `headerAuth()` trusts identity
|
|
214
|
+
headers only with the proxy's secret or from its addresses.
|
|
215
|
+
- Every change needs a page token in a header, an HMAC of the user and their role under the server's
|
|
216
|
+
secret, which another site can neither read nor send. Every refusal is audited.
|
|
217
|
+
- Every response carries a content security policy without inline script, `nosniff`, `no-store`,
|
|
218
|
+
and no referrer. The page inserts every value as text.
|
|
219
|
+
|
|
220
|
+
Findings, each fixed in 2.5 with a test in `studio/test/security.test.ts`:
|
|
221
|
+
- **A request body was read whole before anyone signed in.** Anyone who could reach the port could
|
|
222
|
+
fill the server's memory. A body past `maxBodyBytes`, 1 MiB by default, is now refused with `413`
|
|
223
|
+
as it arrives.
|
|
224
|
+
- **Behind a sign-in proxy that uses Basic authentication, another site could make a change.** Any
|
|
225
|
+
`Authorization` header counted as proof that a script, not a browser, sent the change. But a
|
|
226
|
+
browser attaches Basic credentials to another site's request by itself. Only a bearer token counts
|
|
227
|
+
now, and a change's body must be `application/json`, which a form on another site cannot send.
|
|
228
|
+
- **A change refused for want of the page token was not audited.** It is now, under the person whose
|
|
229
|
+
browser sent it, so a forged change leaves a trace.
|
|
230
|
+
- **A name from the URL could reach an object's prototype.** A graph or review queue named
|
|
231
|
+
`constructor` failed with a server error. Only an application's own entries are looked up.
|
|
232
|
+
- **A body that was not a JSON object reached handlers that read its fields.** `null`, an array, or a
|
|
233
|
+
number failed with a server error. Such a body is now refused with `400`.
|
|
234
|
+
- **A path with malformed percent-encoding failed with a server error.** It is now a `400`.
|
|
235
|
+
- **A viewer could ask a store for everything it holds.** `limit`, `hours`, and `days` had no
|
|
236
|
+
ceiling. A list now returns at most 1,000 items, the audit log 10,000 entries, the issues view 90
|
|
237
|
+
days, and the costs view 366 days; past that, the request is refused with `400`.
|
|
238
|
+
- **The memory journal kept every comment.** It now keeps the latest 10,000, as it keeps audit entries.
|
|
239
|
+
- **The server answered an internal error with its message.** That reply could reach anyone who can
|
|
240
|
+
reach the port. It now says only that the request failed, and logs the error on the server.
|
|
241
|
+
- **A proposal's pull-request link was rendered as stored.** The content security policy already
|
|
242
|
+
blocked a `javascript:` address. Only `http` and `https` links are rendered now.
|
|
243
|
+
|
|
244
|
+
Documented limitations, which the studio guide states:
|
|
245
|
+
- The studio has no tenants of its own. Every account sees every source the studio is given, so a
|
|
246
|
+
tenant's people get a studio over that tenant's `tenantScope()` views.
|
|
247
|
+
- A page token stays valid while the secret, the user, and their role do. Rotate `secret` to revoke
|
|
248
|
+
every page at once.
|
|
249
|
+
- Someone without a session who opens another person's personal link is signed in as that person.
|
|
250
|
+
The page shows who is signed in.
|
|
251
|
+
- `FileStudioJournal` reads its whole file for each listing, so rotate a long-lived one.
|
|
252
|
+
- The playground spends model budget, which is why it needs the editor role.
|
|
253
|
+
|
|
254
|
+
The fuzzing behind these: every route called by every role, and over 2,000 requests with hostile
|
|
255
|
+
paths, queries, and bodies. Each must answer below `500`, and no prototype may be polluted.
|
|
@@ -2,6 +2,7 @@ import type { GateResult } from '../prompts/errors.js';
|
|
|
2
2
|
import type { PromptRegistry } from '../prompts/registry.js';
|
|
3
3
|
import type { PromptHistoryEntry, PromptLabel, PromptStore, PromptVersion, RenderedPrompt, RenderOptions } from '../types/prompts.js';
|
|
4
4
|
import { type ContextDiff } from './diff.js';
|
|
5
|
+
import { type ContextKeyring, type ContextSignature, type ContextSigner } from './signing.js';
|
|
5
6
|
/** A prompt a bundle uses, pinned to one content version of the prompt registry. */
|
|
6
7
|
export interface ContextPromptPin {
|
|
7
8
|
/** The prompt's name in the registry. */
|
|
@@ -61,6 +62,8 @@ export interface ContextBundle extends ContextBundleDefinition {
|
|
|
61
62
|
author?: string;
|
|
62
63
|
/** The version that was newest before it. */
|
|
63
64
|
parent?: string;
|
|
65
|
+
/** Signatures over its content and its pinned prompts' content. Not part of the version. */
|
|
66
|
+
signatures?: ContextSignature[];
|
|
64
67
|
}
|
|
65
68
|
/** Which bundle version is being used, as experiments and traces record it. */
|
|
66
69
|
export interface ContextReference {
|
|
@@ -125,9 +128,33 @@ export interface ContextHubOptions {
|
|
|
125
128
|
gates?: Record<string, readonly ContextPromotionGate[]>;
|
|
126
129
|
/** Called after every recorded change. */
|
|
127
130
|
onChange?: (entry: ContextHistoryEntry) => void | Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Signs every new version, over its content and its pinned prompts' content, so another hub can
|
|
133
|
+
* check where it came from. Signing a bundle that pins prompts needs `prompts`.
|
|
134
|
+
*/
|
|
135
|
+
signer?: ContextSigner;
|
|
136
|
+
/**
|
|
137
|
+
* The keys whose signatures this hub trusts, for `verify()`, and for `requireSignature`. A signature
|
|
138
|
+
* by a key the keyring does not hold is kept, but never trusted.
|
|
139
|
+
*/
|
|
140
|
+
keyring?: ContextKeyring;
|
|
141
|
+
/**
|
|
142
|
+
* Refuses any bundle without a valid signature by a key in `keyring`: on import, and whenever a
|
|
143
|
+
* version is served through `resolve()` or `renderPrompt()`.
|
|
144
|
+
*/
|
|
145
|
+
requireSignature?: boolean;
|
|
128
146
|
/** Replaces the system clock, for tests. */
|
|
129
147
|
now?: () => Date;
|
|
130
148
|
}
|
|
149
|
+
/** What `ContextHub.verify()` found. */
|
|
150
|
+
export interface ContextVerification {
|
|
151
|
+
/** Whether a key in the keyring signed exactly this content. */
|
|
152
|
+
trusted: boolean;
|
|
153
|
+
/** The digest the content has now. */
|
|
154
|
+
digest: string;
|
|
155
|
+
/** The signature that verified, when one did. */
|
|
156
|
+
signature?: ContextSignature;
|
|
157
|
+
}
|
|
131
158
|
/** Base class for context hub errors, each with a stable `code`. */
|
|
132
159
|
export declare class ContextHubError extends Error {
|
|
133
160
|
/** Stable code, such as `CONTEXT_NOT_FOUND`. */
|
|
@@ -160,6 +187,18 @@ export declare class ContextPromotionError extends ContextHubError {
|
|
|
160
187
|
/** Every gate's verdict. */
|
|
161
188
|
results: GateResult[]);
|
|
162
189
|
}
|
|
190
|
+
/** Raised when a bundle has no signature the hub trusts, or its signatures do not match its content. */
|
|
191
|
+
export declare class ContextSignatureError extends ContextHubError {
|
|
192
|
+
/** The bundle. */
|
|
193
|
+
readonly bundle: string;
|
|
194
|
+
/** The version. */
|
|
195
|
+
readonly version: string;
|
|
196
|
+
constructor(
|
|
197
|
+
/** The bundle. */
|
|
198
|
+
bundle: string,
|
|
199
|
+
/** The version. */
|
|
200
|
+
version: string, reason: string);
|
|
201
|
+
}
|
|
163
202
|
/** Raised when a bundle definition, a pin, or an import is invalid. */
|
|
164
203
|
export declare class ContextDefinitionError extends ContextHubError {
|
|
165
204
|
constructor(message: string);
|
|
@@ -182,6 +221,8 @@ export declare class ContextHub {
|
|
|
182
221
|
/** Where versions, labels, and history live. */
|
|
183
222
|
readonly store: PromptStore;
|
|
184
223
|
private readonly now;
|
|
224
|
+
/** Versions already verified as trusted, which never change. */
|
|
225
|
+
private readonly trusted;
|
|
185
226
|
constructor(options?: ContextHubOptions);
|
|
186
227
|
/**
|
|
187
228
|
* Commits a bundle and returns its version. Unchanged content returns the existing version and
|
|
@@ -193,8 +234,15 @@ export declare class ContextHub {
|
|
|
193
234
|
author?: string;
|
|
194
235
|
label?: string | readonly string[];
|
|
195
236
|
}): Promise<ContextBundle>;
|
|
237
|
+
private commitSigned;
|
|
196
238
|
/** A version by content version, label, or `latest`, the default. Throws `ContextNotFoundError` when there is none. */
|
|
197
239
|
get(name: string, ref?: string): Promise<ContextBundle>;
|
|
240
|
+
/**
|
|
241
|
+
* Whether a key in the hub's keyring signed exactly this bundle's content, and its pinned prompts'
|
|
242
|
+
* content as the prompt registry holds them. A version's answer never changes, so it is checked
|
|
243
|
+
* once.
|
|
244
|
+
*/
|
|
245
|
+
verify(bundle: ContextBundle): Promise<ContextVerification>;
|
|
198
246
|
/** A version with the reference experiments and traces record for it. */
|
|
199
247
|
resolve(name: string, ref?: string): Promise<{
|
|
200
248
|
bundle: ContextBundle;
|
|
@@ -262,6 +310,11 @@ export declare class ContextHub {
|
|
|
262
310
|
label?: string | readonly string[];
|
|
263
311
|
by?: string;
|
|
264
312
|
}): Promise<ContextBundle>;
|
|
313
|
+
/** Refuses a bundle without a trusted signature, when the hub requires one. */
|
|
314
|
+
private assertTrusted;
|
|
315
|
+
private check;
|
|
316
|
+
/** What a signature over this bundle covers, with its pinned prompts read from the registry. */
|
|
317
|
+
private signedContent;
|
|
265
318
|
private read;
|
|
266
319
|
private pointer;
|
|
267
320
|
private record;
|