@bivy/bivy 0.10.1-staging.199 → 0.10.1-staging.201
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/dist/protocol/command-spec.js +16 -0
- package/dist/protocol/version.js +51 -0
- package/dist/server.js +28 -5
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Check, Errors } from "typebox/value";
|
|
2
|
+
/**
|
|
3
|
+
* Validate a raw message against a schema. No schema → pass through unchecked
|
|
4
|
+
* (migration-friendly). On failure, returns bounded, path-qualified messages
|
|
5
|
+
* suitable for a `*.error` reply — never throws.
|
|
6
|
+
*/
|
|
7
|
+
export function validateInput(schema, msg) {
|
|
8
|
+
if (!schema)
|
|
9
|
+
return { ok: true, value: msg };
|
|
10
|
+
if (Check(schema, msg))
|
|
11
|
+
return { ok: true, value: msg };
|
|
12
|
+
// typebox v1's validation-error type doesn't surface `path` on its public
|
|
13
|
+
// type, so read it via a narrow cast — matching src/policy/ruleset.ts.
|
|
14
|
+
const errors = [...Errors(schema, msg)].slice(0, 20).map((e) => `${e.path || "/"}: ${e.message}`);
|
|
15
|
+
return { ok: false, errors: errors.length ? errors : ["invalid input"] };
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// Command/event protocol versioning (platform modularization Phase 2 — the
|
|
5
|
+
// "formalize the contract" slice; see docs/internal/platform-modularization-plan.md).
|
|
6
|
+
//
|
|
7
|
+
// The command/event surface is becoming the public API behind every client
|
|
8
|
+
// (CLI, PWA, TUI, SDK), so peers must be able to evolve without silently
|
|
9
|
+
// breaking one another. This module owns the version scalar and the
|
|
10
|
+
// compatibility policy; it is a pure leaf (no imports) so any transport can use
|
|
11
|
+
// it and it is trivially testable.
|
|
12
|
+
/** The wire protocol version this node speaks. Bump when the command/event
|
|
13
|
+
* surface changes in a way clients must be able to detect. */
|
|
14
|
+
export const PROTOCOL_VERSION = 1;
|
|
15
|
+
/** Peers that predate version negotiation present as v0 (today's surface). */
|
|
16
|
+
export const LEGACY_PROTOCOL_VERSION = 0;
|
|
17
|
+
/**
|
|
18
|
+
* Compatibility policy: **serve a compatible subset** (decided 2026-08-11).
|
|
19
|
+
*
|
|
20
|
+
* A newer node does not reject an older client; it serves exactly the
|
|
21
|
+
* operations introduced at or before the client's negotiated version, and
|
|
22
|
+
* withholds newer ones. Legacy (unversioned) clients negotiate to v0 and are
|
|
23
|
+
* served every op with `since <= 0` — i.e. today's surface, unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export function isServedTo(op, clientVersion) {
|
|
26
|
+
return op.since <= clientVersion;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Meet the client where it is. The node speaks up to `PROTOCOL_VERSION`, so the
|
|
30
|
+
* negotiated version is the client's request clamped to [LEGACY, PROTOCOL].
|
|
31
|
+
* A missing/invalid/negative request negotiates to legacy (v0) rather than
|
|
32
|
+
* failing — consistent with the compatible-subset policy.
|
|
33
|
+
*/
|
|
34
|
+
export function negotiateVersion(clientVersion) {
|
|
35
|
+
if (typeof clientVersion !== "number" || !Number.isFinite(clientVersion)) {
|
|
36
|
+
return LEGACY_PROTOCOL_VERSION;
|
|
37
|
+
}
|
|
38
|
+
const requested = Math.trunc(clientVersion);
|
|
39
|
+
if (requested <= LEGACY_PROTOCOL_VERSION)
|
|
40
|
+
return LEGACY_PROTOCOL_VERSION;
|
|
41
|
+
return Math.min(requested, PROTOCOL_VERSION);
|
|
42
|
+
}
|
|
43
|
+
/** Filter a set of versioned operations to the subset a client is served. */
|
|
44
|
+
export function compatibleSubset(ops, clientVersion) {
|
|
45
|
+
const out = new Map();
|
|
46
|
+
for (const [kind, op] of ops) {
|
|
47
|
+
if (isServedTo(op, clientVersion))
|
|
48
|
+
out.set(kind, op);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -17,6 +17,8 @@ import { activeRulesetFor } from "./runtime/ruleset-store.js";
|
|
|
17
17
|
import { createRulesetController } from "./controllers/rulesets.js";
|
|
18
18
|
import { createWorkspaceController } from "./controllers/workspaces.js";
|
|
19
19
|
import { createModelController } from "./controllers/models.js";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { validateInput } from "./protocol/command-spec.js";
|
|
20
22
|
import { collectDiscoveredSessions, planNativeAdoption } from "./runtime/native-session-discovery.js";
|
|
21
23
|
import { aggregateModelCatalog, mergeProviderCatalog } from "./runtime/model-catalog.js";
|
|
22
24
|
import { RuntimeHost, enforcementLevelFor, remoteRuntimeEnabled } from "./runtime/host.js";
|
|
@@ -1687,8 +1689,16 @@ const dedupeSessionNew = (requestId, create) => sessionNewDedupe.run(requestId,
|
|
|
1687
1689
|
const promptDedupe = createSessionNewDedupe();
|
|
1688
1690
|
const dedupePrompt = (clientMessageId, run) => promptDedupe.run(clientMessageId, run);
|
|
1689
1691
|
const RELAY_COMMANDS = {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
+
// First command to adopt boundary validation (the spec form). `requestId` is
|
|
1693
|
+
// the only declared field; typebox permits additional properties by default,
|
|
1694
|
+
// so real pings always pass — this exercises the schema path end-to-end
|
|
1695
|
+
// without changing behavior. Other commands adopt schemas one at a time.
|
|
1696
|
+
ping: {
|
|
1697
|
+
since: 0,
|
|
1698
|
+
schema: Type.Object({ requestId: Type.Optional(Type.String()) }),
|
|
1699
|
+
handler(msg, ctx) {
|
|
1700
|
+
ctx.reply({ type: "pong", requestId: typeof msg.requestId === "string" ? msg.requestId : undefined });
|
|
1701
|
+
},
|
|
1692
1702
|
},
|
|
1693
1703
|
// Kick off `bivy update` on this node from the app's version-mismatch banner
|
|
1694
1704
|
// (see runBivyUpdate). The node restarts itself when the update lands, so the
|
|
@@ -2930,9 +2940,22 @@ const relayCtx = { reply: (event) => relay?.sendEvent(event), broadcast };
|
|
|
2930
2940
|
const RELAY_CLIENT_ID = "relay";
|
|
2931
2941
|
async function handleRelayMessage(msg) {
|
|
2932
2942
|
try {
|
|
2933
|
-
const
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
2943
|
+
const entry = RELAY_COMMANDS[msg.kind];
|
|
2944
|
+
if (entry) {
|
|
2945
|
+
const handler = typeof entry === "function" ? entry : entry.handler;
|
|
2946
|
+
const schema = typeof entry === "function" ? undefined : entry.schema;
|
|
2947
|
+
if (schema) {
|
|
2948
|
+
const check = validateInput(schema, msg);
|
|
2949
|
+
if (!check.ok) {
|
|
2950
|
+
relayCtx.reply({
|
|
2951
|
+
type: `${msg.kind}.error`,
|
|
2952
|
+
requestId: typeof msg.requestId === "string" ? msg.requestId : undefined,
|
|
2953
|
+
error: `Invalid ${msg.kind}: ${check.errors.join("; ")}`,
|
|
2954
|
+
});
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
await handler(msg, relayCtx);
|
|
2936
2959
|
return;
|
|
2937
2960
|
}
|
|
2938
2961
|
// Fallthrough for kinds not in RELAY_COMMANDS: terminal.* frames go to the
|
package/package.json
CHANGED