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.
Files changed (94) hide show
  1. package/API_STABILITY.md +85 -0
  2. package/CHANGELOG.md +147 -1
  3. package/README.md +8 -3
  4. package/SECURITY.md +78 -9
  5. package/dist/context-hub/hub.d.ts +53 -0
  6. package/dist/context-hub/hub.js +108 -3
  7. package/dist/context-hub/index.d.ts +2 -1
  8. package/dist/context-hub/index.js +2 -1
  9. package/dist/context-hub/signing.d.ts +95 -0
  10. package/dist/context-hub/signing.js +88 -0
  11. package/dist/doctor/index.d.ts +4 -2
  12. package/dist/doctor/index.js +29 -0
  13. package/dist/evaluate/threads.d.ts +187 -0
  14. package/dist/evaluate/threads.js +187 -0
  15. package/dist/operations/adapters.d.ts +2 -1
  16. package/dist/operations/runner.js +2 -0
  17. package/dist/operations/stats.js +11 -4
  18. package/dist/postgres/index.d.ts +2 -1
  19. package/dist/postgres/index.js +4 -0
  20. package/dist/postgres/operations.js +11 -4
  21. package/dist/postgres/store.d.ts +7 -0
  22. package/dist/postgres/store.js +21 -0
  23. package/dist/postgres/tenancy.d.ts +43 -0
  24. package/dist/postgres/tenancy.js +71 -0
  25. package/dist/server/deployments.d.ts +127 -12
  26. package/dist/server/deployments.js +402 -37
  27. package/dist/server/metrics.d.ts +2 -0
  28. package/dist/server/metrics.js +7 -0
  29. package/dist/server/runs.d.ts +9 -1
  30. package/dist/server/runs.js +46 -7
  31. package/dist/server/server.js +4 -1
  32. package/dist/server/state.d.ts +12 -2
  33. package/dist/server/state.js +15 -0
  34. package/dist/sqlite/operations.js +15 -4
  35. package/dist/sqlite/store.d.ts +7 -0
  36. package/dist/sqlite/store.js +28 -0
  37. package/dist/store/memory.d.ts +8 -0
  38. package/dist/store/memory.js +12 -1
  39. package/dist/store/redis.d.ts +13 -0
  40. package/dist/store/redis.js +38 -0
  41. package/dist/tracing/instrument.js +1 -0
  42. package/dist/tracing/tracer.d.ts +4 -0
  43. package/dist/tracing/tracer.js +6 -0
  44. package/dist/types/operations.d.ts +7 -0
  45. package/dist/types/server.d.ts +61 -2
  46. package/dist/types/store.d.ts +9 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/dist-cjs/context-hub/hub.d.ts +53 -0
  50. package/dist-cjs/context-hub/hub.js +110 -4
  51. package/dist-cjs/context-hub/index.d.ts +2 -1
  52. package/dist-cjs/context-hub/index.js +7 -1
  53. package/dist-cjs/context-hub/signing.d.ts +95 -0
  54. package/dist-cjs/context-hub/signing.js +96 -0
  55. package/dist-cjs/doctor/index.d.ts +4 -2
  56. package/dist-cjs/doctor/index.js +29 -0
  57. package/dist-cjs/evaluate/threads.d.ts +187 -0
  58. package/dist-cjs/evaluate/threads.js +198 -0
  59. package/dist-cjs/operations/adapters.d.ts +2 -1
  60. package/dist-cjs/operations/runner.js +2 -0
  61. package/dist-cjs/operations/stats.js +11 -4
  62. package/dist-cjs/postgres/index.d.ts +2 -1
  63. package/dist-cjs/postgres/index.js +8 -1
  64. package/dist-cjs/postgres/operations.js +11 -4
  65. package/dist-cjs/postgres/store.d.ts +7 -0
  66. package/dist-cjs/postgres/store.js +21 -0
  67. package/dist-cjs/postgres/tenancy.d.ts +43 -0
  68. package/dist-cjs/postgres/tenancy.js +110 -0
  69. package/dist-cjs/server/deployments.d.ts +127 -12
  70. package/dist-cjs/server/deployments.js +402 -37
  71. package/dist-cjs/server/metrics.d.ts +2 -0
  72. package/dist-cjs/server/metrics.js +8 -0
  73. package/dist-cjs/server/runs.d.ts +9 -1
  74. package/dist-cjs/server/runs.js +45 -6
  75. package/dist-cjs/server/server.js +4 -1
  76. package/dist-cjs/server/state.d.ts +12 -2
  77. package/dist-cjs/server/state.js +15 -0
  78. package/dist-cjs/sqlite/operations.js +15 -4
  79. package/dist-cjs/sqlite/store.d.ts +7 -0
  80. package/dist-cjs/sqlite/store.js +28 -0
  81. package/dist-cjs/store/memory.d.ts +8 -0
  82. package/dist-cjs/store/memory.js +12 -1
  83. package/dist-cjs/store/redis.d.ts +13 -0
  84. package/dist-cjs/store/redis.js +38 -0
  85. package/dist-cjs/tracing/instrument.js +1 -0
  86. package/dist-cjs/tracing/tracer.d.ts +4 -0
  87. package/dist-cjs/tracing/tracer.js +6 -0
  88. package/dist-cjs/types/operations.d.ts +7 -0
  89. package/dist-cjs/types/server.d.ts +61 -2
  90. package/dist-cjs/types/store.d.ts +9 -0
  91. package/dist-cjs/version.d.ts +1 -1
  92. package/dist-cjs/version.js +1 -1
  93. package/llms.txt +1 -1
  94. 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.4.0...HEAD
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
- What comes next is on the [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md):
274
- the gaps 2.4 left open, and the work applications ask for after them.
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 (reviewed; still experimental)
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
- - **Open: two replicas changing one deployment in the same instant can lose one change.** The
181
- server's state store has no atomic compare-and-set. Until it does, deployments stay experimental.
182
- The guide says to make changes from one place, or to pass `expectedVersion` and retry a conflict.
183
- - **Documented limitation: `bucket()` spreads sequential ids unevenly.** Ids that differ only in their
184
- last characters spread unevenly over a few hundred threads. Random ids, which the server gives by
185
- default, spread evenly. The function's output is part of the stability promise, so it is not
186
- changed in 2.x.
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;