Merge nucleic/humble-harbor-viper into dev
This commit is contained in:
@@ -98,6 +98,8 @@ private struct RunnerSection: View {
|
|||||||
.defaultMaxContainers
|
.defaultMaxContainers
|
||||||
@AppStorage(RunnerSettings.balancerPolicyKey) private var balancerPolicy = RunnerBalancerPolicy
|
@AppStorage(RunnerSettings.balancerPolicyKey) private var balancerPolicy = RunnerBalancerPolicy
|
||||||
.leastSessions.rawValue
|
.leastSessions.rawValue
|
||||||
|
@AppStorage(RunnerSettings.intelligenceModeKey) private var intelligenceMode =
|
||||||
|
RunnerIntelligenceMode.mesh.rawValue
|
||||||
// Dev/self-host override; empty ⇒ the production control plane.
|
// Dev/self-host override; empty ⇒ the production control plane.
|
||||||
@AppStorage(RunnerSettings.controlURLKey) private var controlURL = ""
|
@AppStorage(RunnerSettings.controlURLKey) private var controlURL = ""
|
||||||
|
|
||||||
@@ -136,6 +138,22 @@ private struct RunnerSection: View {
|
|||||||
.tag(RunnerBalancerPolicy.roundRobin.rawValue)
|
.tag(RunnerBalancerPolicy.roundRobin.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ANTIMATTER_RUNNER §5, item 6: how the runner replaces Apple Foundation
|
||||||
|
// Models. Rides the same debounced settings push; the container picks it up on
|
||||||
|
// its next boot as NUCLEIC_RUNNER_INTELLIGENCE_MODE.
|
||||||
|
Picker("Runner intelligence", selection: $intelligenceMode) {
|
||||||
|
ForEach(RunnerIntelligenceMode.allCases, id: \.rawValue) { mode in
|
||||||
|
Text(mode.displayName).tag(mode.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"How the runner names chats and writes summaries. “Your devices” delegates "
|
||||||
|
+ "to this Mac / your iPhone over the mesh (background work may run on the "
|
||||||
|
+ "phone; time-sensitive work stays on Macs); “Agent” spends a small model's "
|
||||||
|
+ "tokens on the runner itself. Applies when the runner container next boots.")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
if isDevBuild {
|
if isDevBuild {
|
||||||
TextField(
|
TextField(
|
||||||
"Control URL (dev)", text: $controlURL,
|
"Control URL (dev)", text: $controlURL,
|
||||||
@@ -214,6 +232,7 @@ private struct RunnerSection: View {
|
|||||||
// Knob changes ride to the pool debounced; the DO re-clamps server-side.
|
// Knob changes ride to the pool debounced; the DO re-clamps server-side.
|
||||||
.onChange(of: maxContainers) { _, _ in pushKnobs() }
|
.onChange(of: maxContainers) { _, _ in pushKnobs() }
|
||||||
.onChange(of: balancerPolicy) { _, _ in pushKnobs() }
|
.onChange(of: balancerPolicy) { _, _ in pushKnobs() }
|
||||||
|
.onChange(of: intelligenceMode) { _, _ in pushKnobs() }
|
||||||
.onChange(of: runnerEnabled) { _, on in if on { refreshSharedCredentials() } }
|
.onChange(of: runnerEnabled) { _, on in if on { refreshSharedCredentials() } }
|
||||||
.onAppear { if runnerEnabled { refreshSharedCredentials() } }
|
.onAppear { if runnerEnabled { refreshSharedCredentials() } }
|
||||||
}
|
}
|
||||||
@@ -274,7 +293,8 @@ private struct RunnerSection: View {
|
|||||||
do {
|
do {
|
||||||
try await store.runnerPoolClient().pushSettings(
|
try await store.runnerPoolClient().pushSettings(
|
||||||
maxContainers: maxContainers,
|
maxContainers: maxContainers,
|
||||||
balancerPolicy: RunnerBalancerPolicy(rawValue: balancerPolicy))
|
balancerPolicy: RunnerBalancerPolicy(rawValue: balancerPolicy),
|
||||||
|
intelligenceMode: RunnerIntelligenceMode(rawValue: intelligenceMode))
|
||||||
poolError = nil
|
poolError = nil
|
||||||
} catch {
|
} catch {
|
||||||
poolError = error.localizedDescription
|
poolError = error.localizedDescription
|
||||||
|
|||||||
@@ -1121,11 +1121,20 @@ public enum RunnerSettings {
|
|||||||
/// The runner's `IntelligenceProviding` choice (ANTIMATTER_RUNNER §5): `agent` runs a small
|
/// The runner's `IntelligenceProviding` choice (ANTIMATTER_RUNNER §5): `agent` runs a small
|
||||||
/// SKU of the user's authenticated agent behind strict templates, `mesh` delegates to
|
/// SKU of the user's authenticated agent behind strict templates, `mesh` delegates to
|
||||||
/// Apple-Intelligence-capable devices on the mesh, `heuristic` skips models entirely. Modes
|
/// Apple-Intelligence-capable devices on the mesh, `heuristic` skips models entirely. Modes
|
||||||
/// 1–2 always terminate in the heuristic fallback when they can't serve.
|
/// 1–2 always terminate in the heuristic fallback when they can't serve. Mirrors the
|
||||||
|
/// `IntelligenceMode` union in `cloud/nucleic-runner/src/pool.ts` (keep in lockstep).
|
||||||
public enum RunnerIntelligenceMode: String, CaseIterable, Sendable {
|
public enum RunnerIntelligenceMode: String, CaseIterable, Sendable {
|
||||||
case agent
|
case agent
|
||||||
case mesh
|
case mesh
|
||||||
case heuristic
|
case heuristic
|
||||||
|
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .agent: return "Agent (small model)"
|
||||||
|
case .mesh: return "Your devices (Apple Intelligence)"
|
||||||
|
case .heuristic: return "Off (heuristics only)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A registered git repository plus per-project configuration.
|
/// A registered git repository plus per-project configuration.
|
||||||
|
|||||||
@@ -107,15 +107,17 @@ public actor RunnerPoolClient {
|
|||||||
|
|
||||||
/// Push the Settings knobs. The DO re-clamps server-side (`clampSettings` — same 1…16
|
/// Push the Settings knobs. The DO re-clamps server-side (`clampSettings` — same 1…16
|
||||||
/// bounds as `RunnerSettings.maxContainersRange`; keep them in lockstep), so a stale client
|
/// bounds as `RunnerSettings.maxContainersRange`; keep them in lockstep), so a stale client
|
||||||
/// can't exceed the cap.
|
/// can't exceed the cap. `intelligenceMode` reaches a container as
|
||||||
|
/// `NUCLEIC_RUNNER_INTELLIGENCE_MODE` on its next boot (ANTIMATTER_RUNNER §5, item 6).
|
||||||
public func pushSettings(
|
public func pushSettings(
|
||||||
maxContainers: Int? = nil, balancerPolicy: RunnerBalancerPolicy? = nil,
|
maxContainers: Int? = nil, balancerPolicy: RunnerBalancerPolicy? = nil,
|
||||||
idleTimeoutSeconds: Int? = nil
|
idleTimeoutSeconds: Int? = nil, intelligenceMode: RunnerIntelligenceMode? = nil
|
||||||
) async throws {
|
) async throws {
|
||||||
var patch: [String: Any] = [:]
|
var patch: [String: Any] = [:]
|
||||||
if let maxContainers { patch["maxContainers"] = maxContainers }
|
if let maxContainers { patch["maxContainers"] = maxContainers }
|
||||||
if let balancerPolicy { patch["balancerPolicy"] = balancerPolicy.rawValue }
|
if let balancerPolicy { patch["balancerPolicy"] = balancerPolicy.rawValue }
|
||||||
if let idleTimeoutSeconds { patch["idleTimeoutSeconds"] = idleTimeoutSeconds }
|
if let idleTimeoutSeconds { patch["idleTimeoutSeconds"] = idleTimeoutSeconds }
|
||||||
|
if let intelligenceMode { patch["intelligenceMode"] = intelligenceMode.rawValue }
|
||||||
_ = try await authed("PATCH", "/v1/pool/settings", body: patch)
|
_ = try await authed("PATCH", "/v1/pool/settings", body: patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +259,9 @@ public struct RunnerPoolStatus: Decodable, Sendable, Equatable {
|
|||||||
public let maxContainers: Int
|
public let maxContainers: Int
|
||||||
public let balancerPolicy: String
|
public let balancerPolicy: String
|
||||||
public let idleTimeoutSeconds: Int
|
public let idleTimeoutSeconds: Int
|
||||||
|
/// "mesh" | "agent" | "heuristic" (ANTIMATTER_RUNNER §5); nil from a Worker that
|
||||||
|
/// predates the knob.
|
||||||
|
public let intelligenceMode: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Instance: Decodable, Sendable, Equatable, Identifiable {
|
public struct Instance: Decodable, Sendable, Equatable, Identifiable {
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ import FoundationNetworking
|
|||||||
/// static musl Linux build.
|
/// static musl Linux build.
|
||||||
///
|
///
|
||||||
/// nucleic-smoke '<pairing-code>' --prompt "run `echo hi` in the shell" [--project name]
|
/// nucleic-smoke '<pairing-code>' --prompt "run `echo hi` in the shell" [--project name]
|
||||||
/// [--model sku] [--timeout 300] [--deny]
|
/// [--model sku] [--timeout 300] [--deny] [--expect-intelligence]
|
||||||
|
///
|
||||||
|
/// It also plays an **intelligence worker** (ANTIMATTER_RUNNER §5, item 6): it advertises
|
||||||
|
/// `canProvideIntelligence` (desktop class, so every priority tier is eligible) and answers any
|
||||||
|
/// delegated `intelligenceRequest` with deterministic canned output — no model needed; the point
|
||||||
|
/// is proving the wire path. With `--expect-intelligence` the exercise PASSES when a delegated
|
||||||
|
/// `sessionName` job round-trips end to end: request received here, canned title answered, and
|
||||||
|
/// the host's `sessionUpdated` comes back carrying that exact title. The agent turn itself may
|
||||||
|
/// error (a bare container has no `claude`); the naming path is what's under test.
|
||||||
@main
|
@main
|
||||||
struct Smoke {
|
struct Smoke {
|
||||||
struct Options {
|
struct Options {
|
||||||
@@ -27,8 +35,14 @@ struct Smoke {
|
|||||||
var model: String?
|
var model: String?
|
||||||
var timeoutSeconds: Double = 300
|
var timeoutSeconds: Double = 300
|
||||||
var deny = false // answer approvals with deny instead of allow
|
var deny = false // answer approvals with deny instead of allow
|
||||||
|
/// PASS on a delegated-intelligence round trip instead of on turn completion.
|
||||||
|
var expectIntelligence = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The canned title the worker answers a delegated `sessionName` job with — and the exact
|
||||||
|
/// string `--expect-intelligence` waits to see come back in a `sessionUpdated`.
|
||||||
|
static let cannedSessionTitle = "Mesh Delegation Proven"
|
||||||
|
|
||||||
static func main() async {
|
static func main() async {
|
||||||
var options = Options()
|
var options = Options()
|
||||||
var args = Array(CommandLine.arguments.dropFirst())
|
var args = Array(CommandLine.arguments.dropFirst())
|
||||||
@@ -41,6 +55,7 @@ struct Smoke {
|
|||||||
case "--timeout":
|
case "--timeout":
|
||||||
options.timeoutSeconds = Double(args.isEmpty ? "" : args.removeFirst()) ?? 300
|
options.timeoutSeconds = Double(args.isEmpty ? "" : args.removeFirst()) ?? 300
|
||||||
case "--deny": options.deny = true
|
case "--deny": options.deny = true
|
||||||
|
case "--expect-intelligence": options.expectIntelligence = true
|
||||||
default:
|
default:
|
||||||
if options.pairingCode.isEmpty, arg.contains("d=") { options.pairingCode = arg }
|
if options.pairingCode.isEmpty, arg.contains("d=") { options.pairingCode = arg }
|
||||||
else { fail("unknown argument: \(arg)") }
|
else { fail("unknown argument: \(arg)") }
|
||||||
@@ -48,7 +63,7 @@ struct Smoke {
|
|||||||
}
|
}
|
||||||
guard !options.pairingCode.isEmpty else {
|
guard !options.pairingCode.isEmpty else {
|
||||||
fail("usage: nucleic-smoke '<nucleic://pair?d=…>' [--prompt …] [--project …] "
|
fail("usage: nucleic-smoke '<nucleic://pair?d=…>' [--prompt …] [--project …] "
|
||||||
+ "[--model …] [--timeout seconds] [--deny]")
|
+ "[--model …] [--timeout seconds] [--deny] [--expect-intelligence]")
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload: PairingPayload
|
let payload: PairingPayload
|
||||||
@@ -70,7 +85,14 @@ struct Smoke {
|
|||||||
channel: channel, identity: identity, hostStaticKey: payload.hostStaticKey,
|
channel: channel, identity: identity, hostStaticKey: payload.hostStaticKey,
|
||||||
mode: .pair(secret: payload.pairingSecret),
|
mode: .pair(secret: payload.pairingSecret),
|
||||||
deviceID: DeviceIdentity.hostID(ofStaticKey: identity.staticPublicKey),
|
deviceID: DeviceIdentity.hostID(ofStaticKey: identity.staticPublicKey),
|
||||||
deviceLabel: "nucleic-smoke", scopeClaim: .control)
|
deviceLabel: "nucleic-smoke", scopeClaim: .control,
|
||||||
|
// Offer this process as a mesh AFM worker (ANTIMATTER_RUNNER §5): desktop class so
|
||||||
|
// every priority tier may land here, with a fake top-rank chip so a mesh-mode
|
||||||
|
// runner's queue prefers it. Answers are canned — the wire path is what's tested.
|
||||||
|
clientCaps: WireClientCapabilities(
|
||||||
|
mesh: 1, canProvideIntelligence: true,
|
||||||
|
intelligenceProfile: IntelligenceWorkerProfile(
|
||||||
|
deviceClass: .desktop, chip: "Apple M4 Max")))
|
||||||
|
|
||||||
// Hard deadline for the whole exercise — a hung relay/turn must not wedge CI.
|
// Hard deadline for the whole exercise — a hung relay/turn must not wedge CI.
|
||||||
let timeout = Task {
|
let timeout = Task {
|
||||||
@@ -90,6 +112,7 @@ struct Smoke {
|
|||||||
var sessionID: SessionID?
|
var sessionID: SessionID?
|
||||||
var sawAssistantActivity = false
|
var sawAssistantActivity = false
|
||||||
var approvalsAnswered = 0
|
var approvalsAnswered = 0
|
||||||
|
var intelligenceServed = 0
|
||||||
|
|
||||||
for await event in await client.start() {
|
for await event in await client.start() {
|
||||||
switch event {
|
switch event {
|
||||||
@@ -124,6 +147,14 @@ struct Smoke {
|
|||||||
projectID: project.id, message: options.prompt, model: options.model,
|
projectID: project.id, message: options.prompt, model: options.model,
|
||||||
useWorktree: true, auto: false)))
|
useWorktree: true, auto: false)))
|
||||||
case .sessionUpdated(let summary):
|
case .sessionUpdated(let summary):
|
||||||
|
// The delegated-naming proof (--expect-intelligence): the canned title we
|
||||||
|
// answered the sessionName job with came back as the session's actual name —
|
||||||
|
// the job round-tripped submit → queue → this worker → rename → broadcast.
|
||||||
|
if options.expectIntelligence, summary.title == cannedSessionTitle {
|
||||||
|
log("PASS — delegated intelligence round-tripped: session renamed to "
|
||||||
|
+ "“\(summary.title)” (\(intelligenceServed) request(s) served)")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
// The chat we started is the session that appears (or changes) after startChat.
|
// The chat we started is the session that appears (or changes) after startChat.
|
||||||
if sessionID == nil, startedChat, summary.status.hasTurnInFlight {
|
if sessionID == nil, startedChat, summary.status.hasTurnInFlight {
|
||||||
sessionID = summary.sessionID
|
sessionID = summary.sessionID
|
||||||
@@ -135,6 +166,12 @@ struct Smoke {
|
|||||||
guard summary.sessionID == sessionID else { break }
|
guard summary.sessionID == sessionID else { break }
|
||||||
log("status: \(summary.status.rawValue)")
|
log("status: \(summary.status.rawValue)")
|
||||||
if summary.status == .error {
|
if summary.status == .error {
|
||||||
|
// Under --expect-intelligence the turn is allowed to die (a bare container
|
||||||
|
// has no agent CLI) — the naming push can still land after the error.
|
||||||
|
if options.expectIntelligence {
|
||||||
|
log("session errored (tolerated — waiting on the naming round trip)")
|
||||||
|
break
|
||||||
|
}
|
||||||
log("FAIL — session errored")
|
log("FAIL — session errored")
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
@@ -160,6 +197,11 @@ struct Smoke {
|
|||||||
}
|
}
|
||||||
case .approvalRequested(let approval):
|
case .approvalRequested(let approval):
|
||||||
answer(approval, client: client, deny: options.deny, count: &approvalsAnswered)
|
answer(approval, client: client, deny: options.deny, count: &approvalsAnswered)
|
||||||
|
case .intelligenceRequest(let request):
|
||||||
|
// A mesh-mode runner delegated one AFM job here (ANTIMATTER_RUNNER §5). Render
|
||||||
|
// the shared template (proves the kind decodes) and answer canned output.
|
||||||
|
intelligenceServed += 1
|
||||||
|
await serveIntelligence(request, client: client)
|
||||||
case .credentialNeeded(let need):
|
case .credentialNeeded(let need):
|
||||||
// The runner lacks credentials (ANTIMATTER_RUNNER §6) — play the holding
|
// The runner lacks credentials (ANTIMATTER_RUNNER §6) — play the holding
|
||||||
// device: read the local files, seal each requested kind to the runner's key,
|
// device: read the local files, seal each requested kind to the runner's key,
|
||||||
@@ -181,6 +223,32 @@ struct Smoke {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Answer one delegated intelligence job with deterministic canned output — the worker
|
||||||
|
/// half of the mesh AFM queue, minus the model. `sessionName` answers the canned title the
|
||||||
|
/// `--expect-intelligence` proof watches for; `classifyTurn` answers the template's DONE
|
||||||
|
/// token; anything else gets a one-line placeholder. An unknown kind answers with `error`
|
||||||
|
/// set, exactly as a real executor does.
|
||||||
|
private static func serveIntelligence(
|
||||||
|
_ request: WireIntelligenceRequest, client: SyncClient
|
||||||
|
) async {
|
||||||
|
let priority = request.priority?.rawValue ?? "background"
|
||||||
|
guard IntelligenceDelegate.job(for: request) != nil else {
|
||||||
|
log("intelligence request \(request.kind.rawValue) (\(priority)) — unknown kind, answering error")
|
||||||
|
await client.send(.intelligenceResult(WireIntelligenceResult(
|
||||||
|
id: request.id, error: "unsupported kind: \(request.kind.rawValue)")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let output: String
|
||||||
|
switch request.kind {
|
||||||
|
case .sessionName: output = cannedSessionTitle
|
||||||
|
case .classifyTurn: output = "DONE"
|
||||||
|
default: output = "Delegated \(request.kind.rawValue) output"
|
||||||
|
}
|
||||||
|
log("intelligence request \(request.kind.rawValue) (\(priority)) — answering “\(output)”")
|
||||||
|
await client.send(.intelligenceResult(WireIntelligenceResult(
|
||||||
|
id: request.id, outputs: [output])))
|
||||||
|
}
|
||||||
|
|
||||||
/// Answer a `credentialNeeded`: seal every requested kind we can satisfy from this
|
/// Answer a `credentialNeeded`: seal every requested kind we can satisfy from this
|
||||||
/// machine's own files to the runner's sealing key (AAD = the kind string, so a box can't
|
/// machine's own files to the runner's sealing key (AAD = the kind string, so a box can't
|
||||||
/// be replayed as another kind) and send one provision envelope.
|
/// be replayed as another kind) and send one provision envelope.
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export function buildContainerEnv(base: {
|
|||||||
instanceToken: string;
|
instanceToken: string;
|
||||||
controlPlaneURL: string;
|
controlPlaneURL: string;
|
||||||
relayURL?: string;
|
relayURL?: string;
|
||||||
|
/** How nucleicd replaces AFMs (ANTIMATTER_RUNNER §5, item 6): "mesh" | "agent" |
|
||||||
|
* "heuristic", from the pool settings. Omitted ⇒ nucleicd's own default (mesh). */
|
||||||
|
intelligenceMode?: string;
|
||||||
}): Record<string, string> {
|
}): Record<string, string> {
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
NUCLEIC_RUNNER_POOL_ID: base.poolId,
|
NUCLEIC_RUNNER_POOL_ID: base.poolId,
|
||||||
@@ -38,6 +41,7 @@ export function buildContainerEnv(base: {
|
|||||||
NUCLEIC_RUNNER_CONTROL_URL: base.controlPlaneURL,
|
NUCLEIC_RUNNER_CONTROL_URL: base.controlPlaneURL,
|
||||||
};
|
};
|
||||||
if (base.relayURL) env.NUCLEIC_RELAY_URL = base.relayURL;
|
if (base.relayURL) env.NUCLEIC_RELAY_URL = base.relayURL;
|
||||||
|
if (base.intelligenceMode) env.NUCLEIC_RUNNER_INTELLIGENCE_MODE = base.intelligenceMode;
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ function makeDriver(env: Env): ContainerDriver {
|
|||||||
epoch: extraEnv.NUCLEIC_RUNNER_EPOCH ?? "0",
|
epoch: extraEnv.NUCLEIC_RUNNER_EPOCH ?? "0",
|
||||||
instanceToken: extraEnv.NUCLEIC_RUNNER_INSTANCE_TOKEN ?? "",
|
instanceToken: extraEnv.NUCLEIC_RUNNER_INSTANCE_TOKEN ?? "",
|
||||||
controlPlaneURL: "https://runner.nucleic.blakeslee.xyz",
|
controlPlaneURL: "https://runner.nucleic.blakeslee.xyz",
|
||||||
|
intelligenceMode: extraEnv.NUCLEIC_RUNNER_INTELLIGENCE_MODE,
|
||||||
});
|
});
|
||||||
const res = await stub.fetch("https://container/start", {
|
const res = await stub.fetch("https://container/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
|
|
||||||
export type BalancerPolicy = "least-sessions" | "round-robin";
|
export type BalancerPolicy = "least-sessions" | "round-robin";
|
||||||
|
|
||||||
|
/** How a runner replaces Apple Foundation Models (docs/ANTIMATTER_RUNNER.md §5, item 6).
|
||||||
|
* Mirrors Swift's `RunnerIntelligenceMode` (keep in lockstep). Reaches the container as
|
||||||
|
* `NUCLEIC_RUNNER_INTELLIGENCE_MODE` on its next boot — a settings change never restarts a
|
||||||
|
* running container; the scale-to-zero lifecycle re-boots it soon enough. */
|
||||||
|
export type IntelligenceMode = "mesh" | "agent" | "heuristic";
|
||||||
|
|
||||||
export interface PoolSettings {
|
export interface PoolSettings {
|
||||||
/** Total containers the pool may run at once, host-of-record included. 1 ⇒ tier-0 only:
|
/** Total containers the pool may run at once, host-of-record included. 1 ⇒ tier-0 only:
|
||||||
* every session runs in place inside the host container. Mirrors `nucleic.macvm.maxConcurrent`. */
|
* every session runs in place inside the host container. Mirrors `nucleic.macvm.maxConcurrent`. */
|
||||||
@@ -19,12 +25,14 @@ export interface PoolSettings {
|
|||||||
balancerPolicy: BalancerPolicy;
|
balancerPolicy: BalancerPolicy;
|
||||||
/** Session sandboxes idle (zero sessions) longer than this are reaped by the alarm. */
|
/** Session sandboxes idle (zero sessions) longer than this are reaped by the alarm. */
|
||||||
idleTimeoutSeconds: number;
|
idleTimeoutSeconds: number;
|
||||||
|
intelligenceMode: IntelligenceMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: PoolSettings = {
|
export const DEFAULT_SETTINGS: PoolSettings = {
|
||||||
maxContainers: 4,
|
maxContainers: 4,
|
||||||
balancerPolicy: "least-sessions",
|
balancerPolicy: "least-sessions",
|
||||||
idleTimeoutSeconds: 600,
|
idleTimeoutSeconds: 600,
|
||||||
|
intelligenceMode: "mesh",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_CONTAINERS_RANGE = { min: 1, max: 16 } as const;
|
export const MAX_CONTAINERS_RANGE = { min: 1, max: 16 } as const;
|
||||||
@@ -43,6 +51,14 @@ export function clampSettings(current: PoolSettings, patch: Partial<PoolSettings
|
|||||||
if (patch && typeof patch.idleTimeoutSeconds === "number" && Number.isFinite(patch.idleTimeoutSeconds)) {
|
if (patch && typeof patch.idleTimeoutSeconds === "number" && Number.isFinite(patch.idleTimeoutSeconds)) {
|
||||||
next.idleTimeoutSeconds = Math.min(IDLE_TIMEOUT_RANGE.max, Math.max(IDLE_TIMEOUT_RANGE.min, Math.floor(patch.idleTimeoutSeconds)));
|
next.idleTimeoutSeconds = Math.min(IDLE_TIMEOUT_RANGE.max, Math.max(IDLE_TIMEOUT_RANGE.min, Math.floor(patch.idleTimeoutSeconds)));
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
patch &&
|
||||||
|
(patch.intelligenceMode === "mesh" ||
|
||||||
|
patch.intelligenceMode === "agent" ||
|
||||||
|
patch.intelligenceMode === "heuristic")
|
||||||
|
) {
|
||||||
|
next.intelligenceMode = patch.intelligenceMode;
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +185,10 @@ export class RunnerPoolCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async settings(): Promise<PoolSettings> {
|
private async settings(): Promise<PoolSettings> {
|
||||||
return (await this.state.storage.get<PoolSettings>(K_SETTINGS)) ?? { ...DEFAULT_SETTINGS };
|
// Merge over defaults so a record persisted before a field existed (e.g. an older pool
|
||||||
|
// predating `intelligenceMode`) reads the default rather than `undefined`.
|
||||||
|
const stored = await this.state.storage.get<Partial<PoolSettings>>(K_SETTINGS);
|
||||||
|
return { ...DEFAULT_SETTINGS, ...(stored ?? {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async instances(): Promise<InstanceRecord[]> {
|
private async instances(): Promise<InstanceRecord[]> {
|
||||||
@@ -257,6 +276,7 @@ export class RunnerPoolCore {
|
|||||||
if (existing && existing.state !== "stopped") {
|
if (existing && existing.state !== "stopped") {
|
||||||
return json({ instanceId: "host", epoch: existing.epoch, alreadyRunning: true });
|
return json({ instanceId: "host", epoch: existing.epoch, alreadyRunning: true });
|
||||||
}
|
}
|
||||||
|
const settings = await this.settings();
|
||||||
const epoch = await this.bumpEpoch();
|
const epoch = await this.bumpEpoch();
|
||||||
const token = await this.deps.mintInstanceToken(this.poolId, "host");
|
const token = await this.deps.mintInstanceToken(this.poolId, "host");
|
||||||
await this.deps.driver.start(this.poolId, "host", {
|
await this.deps.driver.start(this.poolId, "host", {
|
||||||
@@ -264,6 +284,7 @@ export class RunnerPoolCore {
|
|||||||
NUCLEIC_RUNNER_INSTANCE_ID: "host",
|
NUCLEIC_RUNNER_INSTANCE_ID: "host",
|
||||||
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
||||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
||||||
|
NUCLEIC_RUNNER_INTELLIGENCE_MODE: settings.intelligenceMode,
|
||||||
});
|
});
|
||||||
const now = this.now();
|
const now = this.now();
|
||||||
const record: InstanceRecord = {
|
const record: InstanceRecord = {
|
||||||
@@ -321,6 +342,7 @@ export class RunnerPoolCore {
|
|||||||
NUCLEIC_RUNNER_INSTANCE_ID: placement.instanceId,
|
NUCLEIC_RUNNER_INSTANCE_ID: placement.instanceId,
|
||||||
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
||||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
||||||
|
NUCLEIC_RUNNER_INTELLIGENCE_MODE: settings.intelligenceMode,
|
||||||
});
|
});
|
||||||
const now = this.now();
|
const now = this.now();
|
||||||
const record: InstanceRecord = {
|
const record: InstanceRecord = {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ test("buildContainerEnv carries the full instance contract", () => {
|
|||||||
instanceToken: "tok",
|
instanceToken: "tok",
|
||||||
controlPlaneURL: "https://runner.example",
|
controlPlaneURL: "https://runner.example",
|
||||||
relayURL: "https://relay.example",
|
relayURL: "https://relay.example",
|
||||||
|
intelligenceMode: "agent",
|
||||||
});
|
});
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
NUCLEIC_RUNNER_POOL_ID: "p1",
|
NUCLEIC_RUNNER_POOL_ID: "p1",
|
||||||
@@ -18,10 +19,11 @@ test("buildContainerEnv carries the full instance contract", () => {
|
|||||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: "tok",
|
NUCLEIC_RUNNER_INSTANCE_TOKEN: "tok",
|
||||||
NUCLEIC_RUNNER_CONTROL_URL: "https://runner.example",
|
NUCLEIC_RUNNER_CONTROL_URL: "https://runner.example",
|
||||||
NUCLEIC_RELAY_URL: "https://relay.example",
|
NUCLEIC_RELAY_URL: "https://relay.example",
|
||||||
|
NUCLEIC_RUNNER_INTELLIGENCE_MODE: "agent",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("relay URL is optional (production default lives in nucleicd)", () => {
|
test("relay URL and intelligence mode are optional (defaults live in nucleicd)", () => {
|
||||||
const env = buildContainerEnv({
|
const env = buildContainerEnv({
|
||||||
poolId: "p1",
|
poolId: "p1",
|
||||||
instanceId: "host",
|
instanceId: "host",
|
||||||
@@ -30,6 +32,7 @@ test("relay URL is optional (production default lives in nucleicd)", () => {
|
|||||||
controlPlaneURL: "https://runner.example",
|
controlPlaneURL: "https://runner.example",
|
||||||
});
|
});
|
||||||
assert.equal("NUCLEIC_RELAY_URL" in env, false);
|
assert.equal("NUCLEIC_RELAY_URL" in env, false);
|
||||||
|
assert.equal("NUCLEIC_RUNNER_INTELLIGENCE_MODE" in env, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("nucleicd control port is pinned (pairing proxy contract)", () => {
|
test("nucleicd control port is pinned (pairing proxy contract)", () => {
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ test("clampSettings bounds maxContainers and idle timeout, keeps unknown-free",
|
|||||||
assert.deepEqual(s4, DEFAULT_SETTINGS);
|
assert.deepEqual(s4, DEFAULT_SETTINGS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("clampSettings accepts only known intelligence modes (item 6)", () => {
|
||||||
|
assert.equal(DEFAULT_SETTINGS.intelligenceMode, "mesh");
|
||||||
|
const agent = clampSettings(DEFAULT_SETTINGS, { intelligenceMode: "agent" });
|
||||||
|
assert.equal(agent.intelligenceMode, "agent");
|
||||||
|
const heuristic = clampSettings(agent, { intelligenceMode: "heuristic" });
|
||||||
|
assert.equal(heuristic.intelligenceMode, "heuristic");
|
||||||
|
// Garbage keeps the current value — the DO never trusts a client.
|
||||||
|
const bogus = clampSettings(agent, { intelligenceMode: "gpt-99" as never });
|
||||||
|
assert.equal(bogus.intelligenceMode, "agent");
|
||||||
|
// A patch that doesn't mention it leaves it untouched.
|
||||||
|
const untouched = clampSettings(agent, { maxContainers: 2 });
|
||||||
|
assert.equal(untouched.intelligenceMode, "agent");
|
||||||
|
});
|
||||||
|
|
||||||
test("least-sessions places on the emptiest live sandbox, ties break deterministically", () => {
|
test("least-sessions places on the emptiest live sandbox, ties break deterministically", () => {
|
||||||
const placement = placeSandbox({
|
const placement = placeSandbox({
|
||||||
instances: [
|
instances: [
|
||||||
@@ -179,12 +193,44 @@ test("provision boots the host container once and is idempotent", async () => {
|
|||||||
assert.equal(first.alreadyRunning, false);
|
assert.equal(first.alreadyRunning, false);
|
||||||
assert.equal(started.length, 1);
|
assert.equal(started.length, 1);
|
||||||
assert.equal(started[0].env.NUCLEIC_RUNNER_INSTANCE_TOKEN, "tok-pool-1-host");
|
assert.equal(started[0].env.NUCLEIC_RUNNER_INSTANCE_TOKEN, "tok-pool-1-host");
|
||||||
|
// Intelligence mode rides every boot (item 6) — the default when never patched.
|
||||||
|
assert.equal(started[0].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "mesh");
|
||||||
|
|
||||||
const second = (await (await core.fetch(req("POST", "/provision", "pool"))).json()) as Record<string, unknown>;
|
const second = (await (await core.fetch(req("POST", "/provision", "pool"))).json()) as Record<string, unknown>;
|
||||||
assert.equal(second.alreadyRunning, true);
|
assert.equal(second.alreadyRunning, true);
|
||||||
assert.equal(started.length, 1);
|
assert.equal(started.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a patched intelligence mode reaches the next container boot (item 6)", async () => {
|
||||||
|
const { core, started } = makeCore();
|
||||||
|
const patched = (await (
|
||||||
|
await core.fetch(req("PATCH", "/settings", "pool", { intelligenceMode: "agent" }))
|
||||||
|
).json()) as { settings: PoolSettings };
|
||||||
|
assert.equal(patched.settings.intelligenceMode, "agent");
|
||||||
|
|
||||||
|
await core.fetch(req("POST", "/provision", "pool"));
|
||||||
|
assert.equal(started[0].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "agent");
|
||||||
|
|
||||||
|
// Sandboxes booted by acquire carry it too.
|
||||||
|
const boot = (await (await core.fetch(req("POST", "/sandbox/acquire", "instance:host"))).json()) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
assert.equal(boot.decision, "boot");
|
||||||
|
assert.equal(started[1].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("settings stored before the intelligenceMode field read the default", async () => {
|
||||||
|
const { core, storage } = makeCore();
|
||||||
|
// A pre-item-6 pool persisted a settings record without the field.
|
||||||
|
await storage.put("settings", { maxContainers: 2, balancerPolicy: "round-robin", idleTimeoutSeconds: 120 });
|
||||||
|
const status = (await (await core.fetch(req("GET", "/status", "pool"))).json()) as {
|
||||||
|
settings: PoolSettings;
|
||||||
|
};
|
||||||
|
assert.equal(status.settings.intelligenceMode, "mesh");
|
||||||
|
assert.equal(status.settings.maxContainers, 2);
|
||||||
|
});
|
||||||
|
|
||||||
test("acquire boots a sandbox, then places on it once it heartbeats ready", async () => {
|
test("acquire boots a sandbox, then places on it once it heartbeats ready", async () => {
|
||||||
let now = NOW;
|
let now = NOW;
|
||||||
const { core, started } = makeCore({ now: () => now });
|
const { core, started } = makeCore({ now: () => now });
|
||||||
|
|||||||
Reference in New Issue
Block a user