@bivy/bivy 0.16.10-staging.2 → 0.16.10-staging.3
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
|
|
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
|
-
|
|
3192
|
-
//
|
|
3193
|
-
//
|
|
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:
|
|
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
|
|
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
|
-
|
|
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