@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.
@@ -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
- ping(msg, ctx) {
1691
- ctx.reply({ type: "pong", requestId: typeof msg.requestId === "string" ? msg.requestId : undefined });
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 command = RELAY_COMMANDS[msg.kind];
2934
- if (command) {
2935
- await command(msg, relayCtx);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.199",
3
+ "version": "0.10.1-staging.201",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",