@bivy/bivy 0.16.10-staging.2 → 0.16.10-staging.4

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.
@@ -16,6 +16,7 @@
16
16
  * issues (and Slack) across many repos/nodes. Pure HTTP + claim/loop logic lives
17
17
  * here; the actual run is injected so the daemon keeps the agent wiring.
18
18
  */
19
+ import { RemoteSessionAdmissionError } from "./session/remote-session-admission.js";
19
20
  /** True when every required tag is among this node's declared capabilities.
20
21
  * A node that fails this must never attempt to claim the item — the hard
21
22
  * block happens locally, before any claim race, so an ineligible node never
@@ -351,6 +352,19 @@ export class ControlPlaneTaskPoller {
351
352
  // A cancelled/lost Run has no node-side terminal transition or retry.
352
353
  if (run.state !== "active")
353
354
  return;
355
+ // Deployment admission is not an agent failure. Never reroute/retry it
356
+ // through a provider policy or mark the work successfully completed.
357
+ if (error instanceof RemoteSessionAdmissionError) {
358
+ const now = new Date().toISOString();
359
+ await report({
360
+ events: [{ at: now, kind: "policy_denial", summary: error.message, reasonCode: error.code, status: "denied", attempt }],
361
+ attention: { severity: "warning", reason: error.message, since: now },
362
+ });
363
+ if (run.state !== "active")
364
+ return;
365
+ await needsAttentionWork(this.cfg, item.id);
366
+ return;
367
+ }
354
368
  const policy = typeof this.policy === "function" ? this.policy(current) : this.policy;
355
369
  const decision = policy?.decide({
356
370
  routing: { runtimeId: current.runtimeId, model: current.model },
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
3
  import fs from "node:fs";
4
+ import { createRemoteSessionAdmission, RemoteSessionAdmissionError } from "./session/remote-session-admission.js";
4
5
  import path from "node:path";
5
6
  import os from "node:os";
6
7
  import { spawn, spawnSync } from "node:child_process";
@@ -2911,17 +2912,6 @@ const RELAY_COMMANDS = {
2911
2912
  });
2912
2913
  return;
2913
2914
  }
2914
- const remoteSessionRequestId = requestId ?? randomUUID();
2915
- const sessionAdmission = await admitRelaySessionCreate(remoteSessionRequestId);
2916
- if (!sessionAdmission.allowed) {
2917
- relay?.sendEvent({
2918
- type: "session.error",
2919
- code: sessionAdmission.code || "remote_session_limit",
2920
- error: sessionAdmission.error,
2921
- requestId,
2922
- });
2923
- return;
2924
- }
2925
2915
  let record;
2926
2916
  try {
2927
2917
  // Deduped by requestId so a client's post-reconnect retry adopts the
@@ -2948,7 +2938,7 @@ const RELAY_COMMANDS = {
2948
2938
  });
2949
2939
  }
2950
2940
  catch (error) {
2951
- relay?.sendEvent({ type: "session.error", error: error instanceof Error ? error.message : String(error) });
2941
+ relay?.sendEvent({ type: "session.error", requestId, code: error instanceof RemoteSessionAdmissionError ? error.code : undefined, error: error instanceof Error ? error.message : String(error) });
2952
2942
  return;
2953
2943
  }
2954
2944
  // Resolve once from the session's actual, now-known launch facts (final
@@ -2977,7 +2967,8 @@ const relayCtx = { reply: (event) => relay?.sendEvent(event), broadcast };
2977
2967
  const RELAY_CLIENT_ID = "relay";
2978
2968
  async function handleRelayMessage(msg) {
2979
2969
  try {
2980
- const dispatched = await clientCommands.dispatch(msg.kind, msg, relayCtx);
2970
+ const requestId = typeof msg.requestId === "string" && msg.requestId.trim() ? msg.requestId : randomUUID();
2971
+ const dispatched = await remoteSessionAdmission.run(JSON.stringify(["relay", identity.nodeId, msg.kind, requestId]), () => clientCommands.dispatch(msg.kind, msg, relayCtx));
2981
2972
  if (dispatched.handled)
2982
2973
  return;
2983
2974
  // Fallthrough for kinds not in RELAY_COMMANDS: terminal.* frames go to the
@@ -3188,24 +3179,25 @@ async function modelAuthFetch(pathname, init = {}) {
3188
3179
  headers.set("content-type", "application/json");
3189
3180
  return fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}${pathname}`, { ...init, headers });
3190
3181
  }
3191
- async function admitRelaySessionCreate(idempotencyKey) {
3192
- // Self-hosted/direct deployments without an account extension stay unrestricted:
3193
- // the control plane answers allowed when no deployment extension is configured.
3182
+ const remoteSessionAdmission = createRemoteSessionAdmission(async (idempotencyKey) => {
3183
+ // Only remote scopes reach this callback. Self-hosted control planes answer
3184
+ // allowed without an extension; losing enrollment mid-launch must fail closed.
3194
3185
  const res = await modelAuthFetch("/node/policy/check", {
3195
3186
  method: "POST",
3196
3187
  body: JSON.stringify({ operation: "session.create", idempotencyKey }),
3188
+ signal: AbortSignal.timeout(10_000),
3197
3189
  });
3198
3190
  if (!res)
3199
- return { allowed: true };
3191
+ return { allowed: false, code: "extension_unavailable", reason: "Reconnect this machine before starting a new remote session." };
3200
3192
  const decision = await res.json().catch(() => ({}));
3201
- if (res.ok && decision.allowed !== false)
3193
+ if (res.ok && decision.allowed === true)
3202
3194
  return { allowed: true };
3203
3195
  return {
3204
3196
  allowed: false,
3205
3197
  code: decision.code,
3206
- error: decision.reason || decision.error || "This account has reached its remote session allowance.",
3198
+ reason: decision.reason || decision.error || "This account has reached its remote session allowance.",
3207
3199
  };
3208
- }
3200
+ });
3209
3201
  // Debounced model-auth sync trigger. A relay wake (`work.available`) fires this
3210
3202
  // so peers answer a new node's vault-key request promptly (event-driven) instead
3211
3203
  // of on the steady 30s poll. Coalesces a burst of wakes into one sync.
@@ -4926,6 +4918,11 @@ function lastUserMessageText(record) {
4926
4918
  return "";
4927
4919
  }
4928
4920
  async function runWorkItem(item, report, signal) {
4921
+ // Stable across delivery/lease retries and node changes. Follow-ups that resume
4922
+ // a session do not consume a slot; any fallback that creates one does.
4923
+ return remoteSessionAdmission.run(JSON.stringify(["automation", item.id]), () => executeWorkItem(item, report, signal));
4924
+ }
4925
+ async function executeWorkItem(item, report, signal) {
4929
4926
  if (signal.aborted)
4930
4927
  throw signal.reason ?? new Error("Run cancelled");
4931
4928
  // Scheduled, manual, and webhook-triggered automations carry the operator's
@@ -7953,6 +7950,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
7953
7950
  if (allowedAgents?.length && !allowedAgents.includes(rt.id)) {
7954
7951
  throw new Error(`Repository policy does not allow agent ${rt.id}`);
7955
7952
  }
7953
+ await remoteSessionAdmission.admit({ resume: Boolean(requestedSessionFile) && !opts.newSession, internal: opts.ephemeral });
7956
7954
  // Optional git-worktree isolation (fresh sessions only). The agent then runs in
7957
7955
  // the worktree, and the A1 boundary confines writes there.
7958
7956
  let worktree = restoredWorktree;
@@ -215,8 +215,8 @@ export function createForkStandUp(deps) {
215
215
  const targetModel = opts.model ?? (targetRuntimeId === bundle.record.runtimeId ? bundle.record.modelRef : undefined);
216
216
  const plan = await deps.materializeFork({ bundle, targetRuntime, ctx: { workspace, cwd, model: targetModel }, seed: { transcriptUrl: opts.transcriptUrl } });
217
217
  const record = plan.kind === "resume"
218
- ? await deps.createSession(cwd, plan.sessionFile, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false })
219
- : await deps.createSession(cwd, undefined, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false });
218
+ ? await deps.createSession(cwd, plan.sessionFile, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true })
219
+ : await deps.createSession(cwd, undefined, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true });
220
220
  // Mark the new session as a fork of its source, so the run card can show
221
221
  // "Forked from …" and the lineage survives a reload (persisted below).
222
222
  record.forkedFrom = bundle.record.sourceSessionId;
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ export class RemoteSessionAdmissionError extends Error {
5
+ code;
6
+ constructor(message, code = "deployment_policy") {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "RemoteSessionAdmissionError";
10
+ }
11
+ }
12
+ /** Scope admission to remote commands and hosted work, not local sessions or
13
+ * history publication. The scope follows async creation helpers (including
14
+ * GitHub/Linear), so a new path cannot forget to forward a billing option.
15
+ * Each creation gets its own slot; retrying the same request reuses those keys.
16
+ * No plan names or allowances belong in Core.
17
+ */
18
+ export function createRemoteSessionAdmission(authorize) {
19
+ const context = new AsyncLocalStorage();
20
+ return {
21
+ run(key, work) {
22
+ return context.run({ key, next: 0 }, work);
23
+ },
24
+ async admit(options = {}) {
25
+ const scope = context.getStore();
26
+ if (!scope || options.resume || options.internal)
27
+ return;
28
+ const key = JSON.stringify([scope.key, scope.next++]);
29
+ let decision;
30
+ try {
31
+ decision = await authorize(key);
32
+ }
33
+ catch {
34
+ throw new RemoteSessionAdmissionError("Remote session admission is temporarily unavailable. Try again shortly.", "extension_unavailable");
35
+ }
36
+ if (decision.allowed !== true) {
37
+ throw new RemoteSessionAdmissionError(decision.reason || "New remote sessions are unavailable for this account.", decision.code);
38
+ }
39
+ },
40
+ };
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.16.10-staging.2",
3
+ "version": "0.16.10-staging.4",
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.",