@cotal-ai/manager 0.45.0 → 0.47.0
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/commands.d.ts +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +9 -9
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/launch.js +2 -2
- package/dist/launch.js.map +1 -1
- package/dist/manager-service-contract.d.ts +21 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +126 -3
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +47 -7
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +263 -62
- package/dist/manager.js.map +1 -1
- package/dist/remote-register.d.ts +4 -0
- package/dist/remote-register.d.ts.map +1 -1
- package/dist/remote-register.js +7 -5
- package/dist/remote-register.js.map +1 -1
- package/dist/run-hosting.d.ts +114 -0
- package/dist/run-hosting.d.ts.map +1 -0
- package/dist/run-hosting.js +462 -0
- package/dist/run-hosting.js.map +1 -0
- package/dist/runtime/custodial-pty.d.ts +16 -0
- package/dist/runtime/custodial-pty.d.ts.map +1 -0
- package/dist/runtime/custodial-pty.js +40 -0
- package/dist/runtime/custodial-pty.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +11 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/pty.d.ts +10 -6
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +12 -5
- package/dist/runtime/pty.js.map +1 -1
- package/dist/static-lifecycle.d.ts +35 -1
- package/dist/static-lifecycle.d.ts.map +1 -1
- package/dist/static-lifecycle.js +65 -0
- package/dist/static-lifecycle.js.map +1 -1
- package/package.json +10 -9
package/dist/manager.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID, randomBytes } from "node:crypto";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { credsAuthenticator } from "@nats-io/transport-node";
|
|
5
5
|
import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
|
|
6
6
|
import { join, dirname, resolve } from "node:path";
|
|
7
|
-
import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
|
|
7
|
+
import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
|
|
8
8
|
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, connectorInstallHint, DEFAULT_CONNECTOR, defaultAgentType, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, saveManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
9
|
-
import { createRuntime, } from "./runtime/index.js";
|
|
9
|
+
import { createRuntime, requireRuntimeAdopt, } from "./runtime/index.js";
|
|
10
10
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
11
11
|
import { makeManagerEndpointEvictionEvidence, makeManagerEndpointEvictor } from "./endpoint-evict.js";
|
|
12
12
|
import { makeManagerHolderLivenessProbe } from "./holder-liveness.js";
|
|
@@ -14,6 +14,7 @@ import { GateReconcileRefused, reconcileEndpointGate } from "./reconcile-gate.js
|
|
|
14
14
|
import { launchSpecForRun, materializePersona, launchAgentToStartOpts, parseLaunchSpec, persistLaunchSpec } from "./launch.js";
|
|
15
15
|
import { authorizeLaunch, authorizeNamedControl } from "./authorize.js";
|
|
16
16
|
import { controlShutdown } from "./control-shutdown.js";
|
|
17
|
+
import { RunHosting } from "./run-hosting.js";
|
|
17
18
|
import { controlSession } from "./control-session.js";
|
|
18
19
|
import { parseResumeCommitArgs, parseResumeControlArgs, parseResumeFinalizeArgs } from "./resume.js";
|
|
19
20
|
// Unit B (the static §13.1 lifecycle executor): the shared grammar/stores from core plus the
|
|
@@ -30,7 +31,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
|
30
31
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
31
32
|
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, registerServiceInstance, deregisterServiceInstance, authorizeServeGrant, writeServiceStatus, SERVICE_READY, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
32
33
|
import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
|
|
33
|
-
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, } from "./static-lifecycle.js";
|
|
34
|
+
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, observeStaticSlot, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
34
35
|
/** Concurrency ceiling — the manager refuses to hold more than this many live + in-flight +
|
|
35
36
|
* cooling slots at once (P4a). Bounds a fork-bomb: spawn is a full agent process per call. */
|
|
36
37
|
const MAX_AGENTS = 50;
|
|
@@ -136,6 +137,8 @@ const FREE_SLOT_CAUSE_TEXT = {
|
|
|
136
137
|
"process-exit": "its own process exited and this manager did not stop it",
|
|
137
138
|
"pi-crash-loop": "this manager retired it after a Pi crash loop",
|
|
138
139
|
"pi-recovery-failed": "this manager retired it after Pi session recovery failed",
|
|
140
|
+
"supervise-crash-loop": "this manager retired it after its supervise restart budget was spent",
|
|
141
|
+
"supervise-recovery-failed": "this manager retired it after a supervised restart failed",
|
|
139
142
|
"session-bind-failed": "this manager stopped it: its host session could not be bound at launch",
|
|
140
143
|
"resume-session-rebind-failed": "this manager stopped it: its host session could not be rebound on resume",
|
|
141
144
|
};
|
|
@@ -319,6 +322,10 @@ export class Manager {
|
|
|
319
322
|
* manager reads its OWN `epgate.<e>.<iid>` epoch over this connection before a terminal commit
|
|
320
323
|
* and skips a superseded commit (the fast-fail belt paired with the (b) barrier-revoke fence). */
|
|
321
324
|
goalWriter;
|
|
325
|
+
/** The workflow-run host (SPEC 14.3): the drives this incarnation holds, each on its own
|
|
326
|
+
* per-run credential and connection. Absent under a remote authority, which mints no driver
|
|
327
|
+
* credentials, so the `run-*` family refuses there rather than connecting on a weaker identity. */
|
|
328
|
+
runHosting;
|
|
322
329
|
/** P2 item 2 must-5 (b): the STABLE goal-writer identity (auth mode) — minted once at
|
|
323
330
|
* registration alongside the serve identity; a renewal re-mints the SAME nkey with a fresh
|
|
324
331
|
* bounded exp and re-stages its distinct credId into the §13.1 revocation family. The current
|
|
@@ -455,6 +462,17 @@ export class Manager {
|
|
|
455
462
|
resumeDurableCommitToken;
|
|
456
463
|
resumedAgentNames = new Set();
|
|
457
464
|
remoteAuthority;
|
|
465
|
+
/** Every broker dial this manager makes, through ONE transport decision.
|
|
466
|
+
*
|
|
467
|
+
* `this.servers` is whatever the mesh record held (`cotal supervise` reads it from there), so it
|
|
468
|
+
* can be a `ws://`/`wss://` broker published through an HTTPS edge. The raw node transport
|
|
469
|
+
* refuses such a URL instead of dialing it, so the scheme picks the dialer here rather than at
|
|
470
|
+
* thirteen call sites. `dialerFor` also owns the websocket transport's options, which is why
|
|
471
|
+
* callers below still pass their own auth options unchanged. */
|
|
472
|
+
dial(opts) {
|
|
473
|
+
const servers = this.servers ?? DEFAULT_SERVER;
|
|
474
|
+
return dialerFor(servers)({ ...opts, servers });
|
|
475
|
+
}
|
|
458
476
|
constructor(opts) {
|
|
459
477
|
this.space = opts.space;
|
|
460
478
|
this.servers = opts.servers;
|
|
@@ -495,6 +513,10 @@ export class Manager {
|
|
|
495
513
|
get runtimeKind() {
|
|
496
514
|
return this.runtime.kind;
|
|
497
515
|
}
|
|
516
|
+
/** Reattach this manager's runtime to a durable handle. Refuses by name when adopt is absent. */
|
|
517
|
+
adoptRuntimeHandle(reference) {
|
|
518
|
+
return requireRuntimeAdopt(this.runtime, reference);
|
|
519
|
+
}
|
|
498
520
|
/** The console page URL (manager-hosted, loopback). */
|
|
499
521
|
get consoleUrl() {
|
|
500
522
|
return this.attach.consoleUrl();
|
|
@@ -699,6 +721,25 @@ export class Manager {
|
|
|
699
721
|
// BEFORE spawn-as-action begins accepting (the goalReconcileDone gate) — a fresh incarnation
|
|
700
722
|
// never drops a goal a dead predecessor accepted. Never fatal; the gate opens either way.
|
|
701
723
|
await this.reconcileGoalIndex();
|
|
724
|
+
// SPEC 14.3: the manager hosts workflow runs. Stood up AFTER registration (it names this
|
|
725
|
+
// instance's coordinates) and reconciled AFTER the goal index, for the same reason: a fresh
|
|
726
|
+
// incarnation takes back every run a dead predecessor was driving before it accepts new ones.
|
|
727
|
+
// The serve surface is already live by here, so the family itself holds the gate: `runHost()`
|
|
728
|
+
// refuses `run-start`/`run-resume` as `unavailable` until the host exists and `RunHosting`
|
|
729
|
+
// refuses them until its reconcile has returned. A user-auth mesh stands no host up at all
|
|
730
|
+
// (`runHost()` names why); a remote-authority manager holds no signer to mint with.
|
|
731
|
+
if (!this.remoteAuthority && !this.userMode) {
|
|
732
|
+
this.runHosting = new RunHosting({
|
|
733
|
+
space: this.space,
|
|
734
|
+
servers: this.servers,
|
|
735
|
+
endpoint: MANAGER_ENDPOINT,
|
|
736
|
+
instanceId: this.managerInstanceId,
|
|
737
|
+
holder: { id: this.ep.ref().id, lifecycleUid: this.managerLifecycleUid },
|
|
738
|
+
auth: this.auth,
|
|
739
|
+
log: (line) => console.error(line),
|
|
740
|
+
});
|
|
741
|
+
await this.runHosting.reconcile();
|
|
742
|
+
}
|
|
702
743
|
// Plane-3 (durable backstop) is NOT the manager's job — the manager only manages agent lifecycle.
|
|
703
744
|
// The server-side delivery daemon hosts the fan-out writer + trusted reader, owns the durable
|
|
704
745
|
// membership registry, and serves the runtime durable join/leave/list ops (on `ctl.delivery`). The
|
|
@@ -835,6 +876,9 @@ export class Manager {
|
|
|
835
876
|
// scoped executor. Without this the standing session-ledger connection dies at its TTL and
|
|
836
877
|
// `attach` stops establishing sessions until a restart. The connection's authenticator presents
|
|
837
878
|
// the refreshed credential on its next (re)connect.
|
|
879
|
+
// SPEC 14.6: every hosted drive's per-run `run-driver` credential is the manager's to renew
|
|
880
|
+
// for the same nkey; a run parked in a pause for days must not die at the credential's TTL.
|
|
881
|
+
await this.runHosting?.renew();
|
|
838
882
|
if (this.sessionLedgerConn && this.sessionLedgerCreds && this.auth) {
|
|
839
883
|
const sw = this.sessionLedgerConn;
|
|
840
884
|
try {
|
|
@@ -1121,8 +1165,6 @@ export class Manager {
|
|
|
1121
1165
|
async awaitHandleExit(handle) {
|
|
1122
1166
|
if (!handle.waitForExit)
|
|
1123
1167
|
throw new Error(`runtime "${handle.kind}" cannot prove child exit (AgentHandle.waitForExit is not implemented)`);
|
|
1124
|
-
if (handle.status() === "exited")
|
|
1125
|
-
return;
|
|
1126
1168
|
await withTimeout(handle.waitForExit(), this.preserveStopTimeoutMs, `child did not exit within ${this.preserveStopTimeoutMs}ms`);
|
|
1127
1169
|
if (handle.status() !== "exited")
|
|
1128
1170
|
throw new Error(`runtime "${handle.kind}" reported exit completion but status is still running`);
|
|
@@ -1336,6 +1378,10 @@ export class Manager {
|
|
|
1336
1378
|
// in-flight command can write a status back onto the record it just removed.
|
|
1337
1379
|
const registered = this.serviceServe !== undefined;
|
|
1338
1380
|
await this.stopServiceServe();
|
|
1381
|
+
// The drives AFTER the serve loop (no new `run-start` can land) and BEFORE deregistration: a
|
|
1382
|
+
// released run's status write is the last thing this incarnation says about it.
|
|
1383
|
+
await this.runHosting?.stop();
|
|
1384
|
+
this.runHosting = undefined;
|
|
1339
1385
|
if (registered)
|
|
1340
1386
|
await this.deregisterServiceOnStop();
|
|
1341
1387
|
await this.stopGoalWriter();
|
|
@@ -1759,6 +1805,35 @@ export class Manager {
|
|
|
1759
1805
|
* cross-owner persona writes - an operator redefines via config, not the wire), where the ctl
|
|
1760
1806
|
* admin tier allowed operator cross-owner redefine; (2) launch is owner-equality-only, above.
|
|
1761
1807
|
* Both are least-privilege reductions, never widenings. */
|
|
1808
|
+
/** The run host, or one of three refusals. A remote-authority manager holds no space signer, so
|
|
1809
|
+
* it cannot mint the per-run driver credential SPEC 14.6 requires, and hosting on any other
|
|
1810
|
+
* identity would be the fallback this tree does not take: `unimplemented`, for good. A
|
|
1811
|
+
* user-auth mesh is `unimplemented` too, for a different reason it names: a hosted run's seats
|
|
1812
|
+
* are spawned, turned and despawned by a caller derived from the run id under the static
|
|
1813
|
+
* owner, which the user-mode spawn door refuses (no `u_` owner), so a program would fail at
|
|
1814
|
+
* its first seat; no path to `--local` is offered there since a user bearer holds no run rows
|
|
1815
|
+
* either. An ordinary manager whose host is not standing yet is still booting (the serve
|
|
1816
|
+
* surface comes up before the host): `unavailable`, retry. The three are told apart, since
|
|
1817
|
+
* the first sentence steers a caller to `--local` and the others must not. */
|
|
1818
|
+
runHost() {
|
|
1819
|
+
if (this.runHosting)
|
|
1820
|
+
return this.runHosting;
|
|
1821
|
+
if (this.remoteAuthority)
|
|
1822
|
+
throw new EpEnvelopeError("unimplemented", "this manager does not host workflow runs: a remote-authority manager mints no run-driver credentials (SPEC 14.6); drive the run from a terminal with `cotal run start --local --file <program>`");
|
|
1823
|
+
if (this.userMode)
|
|
1824
|
+
throw new EpEnvelopeError("unimplemented", `user-auth space "${this.space}" hosts no workflow runs yet: a hosted run's seats would be spawned under the static owner, which a user mesh refuses; run programs on a static-auth mesh`);
|
|
1825
|
+
throw new EpEnvelopeError("unavailable", "the manager is still booting its workflow-run host; retry shortly (SPEC 14.3)");
|
|
1826
|
+
}
|
|
1827
|
+
/** The answerer a `run-answer` records (SPEC 14.5): the caller as this manager knows them. A
|
|
1828
|
+
* managed seat is named by its persona name; any other authenticated caller (an operator
|
|
1829
|
+
* instrument, a logged-in user) by its principal. Never the request body's word. */
|
|
1830
|
+
runAnswerer(ctx) {
|
|
1831
|
+
const caller = principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
|
|
1832
|
+
for (const a of this.agents.values())
|
|
1833
|
+
if (this.managedPrincipal(a) === caller)
|
|
1834
|
+
return a.name;
|
|
1835
|
+
return caller;
|
|
1836
|
+
}
|
|
1762
1837
|
managerServiceDefs() {
|
|
1763
1838
|
const args = (ctx) => (ctx.request.args ?? {});
|
|
1764
1839
|
const callerOf = (ctx) => principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
|
|
@@ -1794,8 +1869,34 @@ export class Manager {
|
|
|
1794
1869
|
inspect: (ctx) => this.serveGated(ctx, async () => {
|
|
1795
1870
|
const name = String(args(ctx).name ?? "").trim();
|
|
1796
1871
|
const row = this.list(await this.psOwnerFilter(callerOf(ctx), false)).find((x) => x.name === name);
|
|
1797
|
-
if (!row)
|
|
1872
|
+
if (!row) {
|
|
1873
|
+
// The live hit above stays entirely local. Only a miss widens into durable static state,
|
|
1874
|
+
// where `not-found` is honest only after the slot read itself succeeds and returns absent.
|
|
1875
|
+
// User-mode lifecycles have no manager-local mgrslot row; their per-name projection remains
|
|
1876
|
+
// the live map until the auth service exposes an equivalent mediated reader.
|
|
1877
|
+
if (this.auth && !this.userMode) {
|
|
1878
|
+
const recordsKv = this.goalWriter?.ctx.kv;
|
|
1879
|
+
if (!recordsKv)
|
|
1880
|
+
throw new EpEnvelopeError("unavailable", `the durable static slot store is not ready while inspecting "${name}"`, [
|
|
1881
|
+
{ kind: STATIC_SLOT_READ_FAILED_DETAIL, name, record: "slot", operation: "read" },
|
|
1882
|
+
]);
|
|
1883
|
+
let observed;
|
|
1884
|
+
try {
|
|
1885
|
+
observed = await observeStaticSlot(recordsKv, DEV_OWNER, name, this.managerInstanceId);
|
|
1886
|
+
}
|
|
1887
|
+
catch (error) {
|
|
1888
|
+
throw new EpEnvelopeError("unavailable", `the durable static slot state for "${name}" could not be read: ${error.message}`, [
|
|
1889
|
+
{ kind: STATIC_SLOT_READ_FAILED_DETAIL, name, record: error instanceof StaticSlotReadError ? error.record : "slot-or-head", operation: "read" },
|
|
1890
|
+
]);
|
|
1891
|
+
}
|
|
1892
|
+
// A retired row affirms that no agent exists now, so it keeps the old `not-found`
|
|
1893
|
+
// result. The changed refusal is reserved for durable NONTERMINAL state that contradicts
|
|
1894
|
+
// the live-map miss and needs operator attention.
|
|
1895
|
+
if (observed !== undefined && observed.slotPhase !== "retired")
|
|
1896
|
+
throw new EpEnvelopeError("failed-precondition", `no live agent "${name}"; durable state contradicts absence ${renderStaticSlotObservation(observed)}`, [observed]);
|
|
1897
|
+
}
|
|
1798
1898
|
throw new EpEnvelopeError("not-found", `no agent "${name}"`);
|
|
1899
|
+
}
|
|
1799
1900
|
return row;
|
|
1800
1901
|
}),
|
|
1801
1902
|
models: (ctx) => this.serveGated(ctx, async () => {
|
|
@@ -1873,6 +1974,13 @@ export class Manager {
|
|
|
1873
1974
|
resumePreserved: (ctx) => adminGated(ctx, async () => unwrap(await this.opResumePreserved(args(ctx)))),
|
|
1874
1975
|
commitResume: (ctx) => adminGated(ctx, async () => unwrap(await this.opCommitResume(args(ctx)))),
|
|
1875
1976
|
finalizeResume: (ctx) => adminGated(ctx, async () => unwrap(await this.opFinalizeResume(args(ctx)))),
|
|
1977
|
+
// The workflow-run family (SPEC 14.3): the manager hosts the driver. Reach is the broker's
|
|
1978
|
+
// (`run` capability / privileged instrument rows); the serve gate is the maintenance fence.
|
|
1979
|
+
runStart: (ctx) => this.serveGated(ctx, () => this.runHost().start(args(ctx))),
|
|
1980
|
+
runResume: (ctx) => this.serveGated(ctx, () => this.runHost().resume(args(ctx))),
|
|
1981
|
+
runAnswer: (ctx) => this.serveGated(ctx, () => this.runHost().answer(args(ctx), this.runAnswerer(ctx))),
|
|
1982
|
+
runStatus: (ctx) => this.serveGated(ctx, () => this.runHost().status(args(ctx))),
|
|
1983
|
+
runPs: (ctx) => this.serveGated(ctx, () => this.runHost().list(args(ctx))),
|
|
1876
1984
|
preparePreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("preparePreservation", args(ctx)))),
|
|
1877
1985
|
commitPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("commitPreservation", args(ctx)))),
|
|
1878
1986
|
abortPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("abortPreservation", args(ctx)))),
|
|
@@ -2101,7 +2209,7 @@ export class Manager {
|
|
|
2101
2209
|
// vocabulary as static capabilities; the broker maps them to the ctl tiers. `role:<r>` tokens
|
|
2102
2210
|
// pass through too (a persona may hold delegable roles) — the ledger's envelope walk still
|
|
2103
2211
|
// attenuates every one of these against the spawner chain.
|
|
2104
|
-
const scope = (opts.capabilities ?? []).filter((c) => c === "spawn" || c === "admin" || /^role:[A-Za-z0-9_-]+$/.test(c));
|
|
2212
|
+
const scope = (opts.capabilities ?? []).filter((c) => c === "spawn" || c === "run" || c === "admin" || /^role:[A-Za-z0-9_-]+$/.test(c));
|
|
2105
2213
|
// The manager's ONE store (injected for a hosted composition, workstation FS locally). A hosted
|
|
2106
2214
|
// user-mode spawn reads the callout material from it — the same store the auth-store kinds
|
|
2107
2215
|
// (callout/issuer/…) were migrated onto — so this is no longer a local-only path.
|
|
@@ -2441,7 +2549,7 @@ export class Manager {
|
|
|
2441
2549
|
const creds = await mintCreds(this.auth, newIdentity(), "retirement-requester", {
|
|
2442
2550
|
retirementRequester: { ...caller, target: { owner: target.owner, actor: target.actor, lifecycleUid: a.lifecycleUid } },
|
|
2443
2551
|
});
|
|
2444
|
-
const nc = await
|
|
2552
|
+
const nc = await this.dial({ authenticator: credsAuthenticator(new TextEncoder().encode(creds)), maxReconnectAttempts: 0 });
|
|
2445
2553
|
try {
|
|
2446
2554
|
// §13.2 nonce: >=128 bits of CSPRNG entropy, base64url (the `endpoint-invoke` idiom).
|
|
2447
2555
|
const nonce = randomBytes(24).toString("base64url");
|
|
@@ -2635,9 +2743,10 @@ export class Manager {
|
|
|
2635
2743
|
}
|
|
2636
2744
|
throw new Error(`replacement did not prove session ${expected} (${last})`);
|
|
2637
2745
|
}
|
|
2638
|
-
/** Restart one
|
|
2639
|
-
*
|
|
2640
|
-
*
|
|
2746
|
+
/** Restart one managed process in place. Identity, lifecycle, credentials, durables, children,
|
|
2747
|
+
* and the manager row remain owned; only the process handle/control endpoint change. Budget
|
|
2748
|
+
* comes from `restart.policy` when spawn carried `supervise`; otherwise the Pi session-recovery
|
|
2749
|
+
* constants. Spending the budget falls through to normal retirement. */
|
|
2641
2750
|
recoverManagedSession(a) {
|
|
2642
2751
|
const restart = a.restart;
|
|
2643
2752
|
if (!restart || !restart.armed || restart.recovering || a.terminalizing)
|
|
@@ -2646,12 +2755,15 @@ export class Manager {
|
|
|
2646
2755
|
if (!release)
|
|
2647
2756
|
return; // preservation owns the cut once the lifecycle fence closes
|
|
2648
2757
|
const now = Date.now();
|
|
2649
|
-
|
|
2758
|
+
const limit = restart.policy?.restarts ?? SESSION_RESTART_LIMIT;
|
|
2759
|
+
const windowMs = restart.policy?.windowMs ?? SESSION_RESTART_WINDOW_MS;
|
|
2760
|
+
const supervised = restart.policy !== undefined;
|
|
2761
|
+
restart.crashes = restart.crashes.filter((at) => now - at < windowMs);
|
|
2650
2762
|
restart.crashes.push(now);
|
|
2651
|
-
if (restart.crashes.length >
|
|
2652
|
-
console.error(`! ${a.name}: Pi crash loop (${restart.crashes.length} crashes in ${
|
|
2763
|
+
if (restart.crashes.length > limit) {
|
|
2764
|
+
console.error(`! ${a.name}: ${supervised ? "supervised" : "Pi"} crash loop (${restart.crashes.length} crashes in ${windowMs / 1000}s) - retiring the managed seat`);
|
|
2653
2765
|
restart.armed = false;
|
|
2654
|
-
this.freeSlot(a, true, "pi-crash-loop");
|
|
2766
|
+
this.freeSlot(a, true, supervised ? "supervise-crash-loop" : "pi-crash-loop");
|
|
2655
2767
|
this.reapChildrenOf(this.managedPrincipal(a));
|
|
2656
2768
|
release();
|
|
2657
2769
|
return;
|
|
@@ -2660,21 +2772,40 @@ export class Manager {
|
|
|
2660
2772
|
void (async () => {
|
|
2661
2773
|
let replacement;
|
|
2662
2774
|
try {
|
|
2663
|
-
const sessionId = this.readManagedSession(a);
|
|
2664
2775
|
const connector = await this.resolveConnector(a.agent);
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
...restart.opts,
|
|
2669
|
-
resume: undefined,
|
|
2670
|
-
prompt: undefined,
|
|
2671
|
-
continueSession: sessionId,
|
|
2672
|
-
};
|
|
2776
|
+
const continueSession = connector.supportsSessionContinuation ? this.readManagedSession(a) : undefined;
|
|
2777
|
+
const opts = continueSession !== undefined
|
|
2778
|
+
? { ...restart.opts, resume: undefined, prompt: undefined, continueSession }
|
|
2779
|
+
: { ...restart.opts, resume: undefined, prompt: undefined };
|
|
2673
2780
|
const spec = connector.buildLaunch(opts);
|
|
2781
|
+
const wanted = this.managedPrincipal(a);
|
|
2782
|
+
const joinedAfter = this.ep.getRoster()
|
|
2783
|
+
.filter((p) => p.card.id === wanted && p.lifecycleUid === a.lifecycleUid)
|
|
2784
|
+
.reduce((max, p) => Math.max(max, p.ts), 0) + 1;
|
|
2674
2785
|
const handle = this.runtime.spawn(a.name, spec, a.launch.cwd);
|
|
2675
2786
|
replacement = handle;
|
|
2676
2787
|
restart.sessionStatePath = spec.sessionStatePath ?? restart.sessionStatePath;
|
|
2677
|
-
|
|
2788
|
+
if (continueSession !== undefined)
|
|
2789
|
+
await this.awaitRecoveredSession(a, continueSession, handle, spec.control);
|
|
2790
|
+
else {
|
|
2791
|
+
const previousHandle = a.handle;
|
|
2792
|
+
const previousControl = a.control;
|
|
2793
|
+
a.handle = handle;
|
|
2794
|
+
a.control = spec.control;
|
|
2795
|
+
try {
|
|
2796
|
+
const readiness = await this.awaitReadiness(a, connector.readinessTimeoutMs ?? this.readinessTimeoutMs, {
|
|
2797
|
+
reapOnExit: false,
|
|
2798
|
+
joinedAfter,
|
|
2799
|
+
});
|
|
2800
|
+
if (!readiness.ok)
|
|
2801
|
+
throw new Error(readiness.detail);
|
|
2802
|
+
}
|
|
2803
|
+
catch (error) {
|
|
2804
|
+
a.handle = previousHandle;
|
|
2805
|
+
a.control = previousControl;
|
|
2806
|
+
throw error;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2678
2809
|
if (this.agents.get(a.name) !== a || a.terminalizing) {
|
|
2679
2810
|
try {
|
|
2680
2811
|
handle.stop({ graceful: false });
|
|
@@ -2687,7 +2818,10 @@ export class Manager {
|
|
|
2687
2818
|
replacement = undefined;
|
|
2688
2819
|
restart.opts = opts;
|
|
2689
2820
|
restart.recovering = false;
|
|
2690
|
-
|
|
2821
|
+
if (continueSession !== undefined)
|
|
2822
|
+
console.error(`! ${a.name}: recovered Pi session ${continueSession} after crash (${restart.crashes.length}/${limit})`);
|
|
2823
|
+
else
|
|
2824
|
+
console.error(`! ${a.name}: restarted under the same lifecycle after crash (${restart.crashes.length}/${limit})`);
|
|
2691
2825
|
this.watchExit(a);
|
|
2692
2826
|
}
|
|
2693
2827
|
catch (error) {
|
|
@@ -2698,14 +2832,14 @@ export class Manager {
|
|
|
2698
2832
|
tail = this.tail(await (replacement ?? a.handle).attach().backlog());
|
|
2699
2833
|
}
|
|
2700
2834
|
catch { /* runtime has no readable tail */ }
|
|
2701
|
-
console.error(`! ${a.name}: Pi session recovery failed: ${error.message}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
|
|
2702
|
-
// The replacement may be alive but unable to prove
|
|
2835
|
+
console.error(`! ${a.name}: ${supervised ? "supervised restart" : "Pi session recovery"} failed: ${error.message}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
|
|
2836
|
+
// The replacement may be alive but unable to prove readiness. Stop it BEFORE
|
|
2703
2837
|
// retiring credentials/durables; otherwise an untracked process survives under torn auth.
|
|
2704
2838
|
try {
|
|
2705
2839
|
replacement?.stop({ graceful: false });
|
|
2706
2840
|
}
|
|
2707
2841
|
catch { /* terminal cleanup continues */ }
|
|
2708
|
-
this.freeSlot(a, true, "pi-recovery-failed");
|
|
2842
|
+
this.freeSlot(a, true, supervised ? "supervise-recovery-failed" : "pi-recovery-failed");
|
|
2709
2843
|
this.reapChildrenOf(this.managedPrincipal(a));
|
|
2710
2844
|
}
|
|
2711
2845
|
finally {
|
|
@@ -2714,13 +2848,23 @@ export class Manager {
|
|
|
2714
2848
|
})();
|
|
2715
2849
|
}
|
|
2716
2850
|
/** A managed agent's process exited on its own (crash, /exit, finished). Continuation-capable Pi
|
|
2717
|
-
* seats restart in place after readiness;
|
|
2851
|
+
* seats restart in place after readiness; a spawn carrying `supervise` restarts any connector
|
|
2852
|
+
* the same way, without classifying a session-state file. Every other exit follows the existing
|
|
2853
|
+
* terminal path. */
|
|
2718
2854
|
onAgentExit(a) {
|
|
2719
2855
|
// Preservation owns the child-stop snapshot. Exit watchers must neither delete that snapshot nor
|
|
2720
2856
|
// trigger normal deprovision/reap while the cut is being formed.
|
|
2721
2857
|
if (this.maintenanceState !== "active")
|
|
2722
2858
|
return;
|
|
2859
|
+
// A replacement is proving readiness under this row. Its own wait owns a failed
|
|
2860
|
+
// relaunch; treating that exit as a seat death would free the slot mid-recovery.
|
|
2861
|
+
if (a.restart?.recovering && !a.terminalizing)
|
|
2862
|
+
return;
|
|
2723
2863
|
if (a.restart?.armed && !a.terminalizing) {
|
|
2864
|
+
if (a.restart.policy !== undefined) {
|
|
2865
|
+
this.recoverManagedSession(a);
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2724
2868
|
try {
|
|
2725
2869
|
if (this.readManagedSessionState(a).status === "running") {
|
|
2726
2870
|
this.recoverManagedSession(a);
|
|
@@ -2808,6 +2952,21 @@ export class Manager {
|
|
|
2808
2952
|
// scalar/array (the CLI never does). Core doesn't interpret the keys; the connector validates them.
|
|
2809
2953
|
if (args.launchOptions !== undefined && (typeof args.launchOptions !== "object" || args.launchOptions === null || Array.isArray(args.launchOptions)))
|
|
2810
2954
|
return Promise.resolve({ ok: false, error: "launchOptions: expected a key:value mapping" });
|
|
2955
|
+
let supervise;
|
|
2956
|
+
if (args.supervise !== undefined) {
|
|
2957
|
+
const raw = args.supervise;
|
|
2958
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
2959
|
+
return Promise.resolve({ ok: false, error: "supervise: expected { restarts, windowMs }" });
|
|
2960
|
+
const rec = raw;
|
|
2961
|
+
const extra = Object.keys(rec).filter((k) => k !== "restarts" && k !== "windowMs");
|
|
2962
|
+
if (extra.length > 0)
|
|
2963
|
+
return Promise.resolve({ ok: false, error: `supervise: unknown key ${extra[0]}; it takes restarts and windowMs` });
|
|
2964
|
+
if (typeof rec.restarts !== "number" || !Number.isInteger(rec.restarts) || rec.restarts < 1)
|
|
2965
|
+
return Promise.resolve({ ok: false, error: "supervise.restarts: expected a positive integer" });
|
|
2966
|
+
if (typeof rec.windowMs !== "number" || !Number.isInteger(rec.windowMs) || rec.windowMs < 1)
|
|
2967
|
+
return Promise.resolve({ ok: false, error: "supervise.windowMs: expected a positive integer" });
|
|
2968
|
+
supervise = { restarts: rec.restarts, windowMs: rec.windowMs };
|
|
2969
|
+
}
|
|
2811
2970
|
// ACL overrides arrive as string arrays or not at all — a malformed value is a bad request,
|
|
2812
2971
|
// not something to coerce (no fallbacks).
|
|
2813
2972
|
const strList = (v, flag) => {
|
|
@@ -2844,6 +3003,7 @@ export class Manager {
|
|
|
2844
3003
|
allowSubscribe,
|
|
2845
3004
|
allowPublish,
|
|
2846
3005
|
shareTools: args.shareTools !== undefined ? String(args.shareTools) : undefined,
|
|
3006
|
+
...(supervise !== undefined ? { supervise } : {}),
|
|
2847
3007
|
}, caller, hooks);
|
|
2848
3008
|
}
|
|
2849
3009
|
/** Resolve a connector by agent type. Library composition (installedExtensions off) → a registry
|
|
@@ -3165,6 +3325,16 @@ export class Manager {
|
|
|
3165
3325
|
// reject-before-side-effects window as the harness preflight above; buildLaunch stays the backstop.
|
|
3166
3326
|
if (opts.resume && !connector.supportsResume)
|
|
3167
3327
|
return { ok: false, error: `${agent} connector does not support resuming an existing session (resume)` };
|
|
3328
|
+
// A restart policy this host cannot honour is refused at accept, never accepted and ignored.
|
|
3329
|
+
// External runtimes (tmux/cmux/orca/herdr) attach to a process they do not own and stream no
|
|
3330
|
+
// exit, so a name cannot be respawned in place. User-mode seats have no static slot that
|
|
3331
|
+
// keeps the incarnation owned across a process death, so the same refusal applies there.
|
|
3332
|
+
if (opts.supervise !== undefined) {
|
|
3333
|
+
if (this.runtime.kind !== "pty")
|
|
3334
|
+
return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${this.runtime.kind}" cannot respawn a name in place` };
|
|
3335
|
+
if (this.userMode)
|
|
3336
|
+
return { ok: false, error: "supervise is a restart policy this host cannot enforce: a user-mode seat has no static slot to keep the incarnation owned across a process death" };
|
|
3337
|
+
}
|
|
3168
3338
|
// Resolve the launch profile: IDENTITY (free-form `name:`) + role + read/post ACL + capabilities
|
|
3169
3339
|
// + model/variant. Either from a fully-resolved manifest launch object (`opts.resolved`, whose `config`
|
|
3170
3340
|
// is a materialized transient persona — the file is NOT the access authority), or from the
|
|
@@ -3577,8 +3747,17 @@ export class Manager {
|
|
|
3577
3747
|
? Object.keys(opts.launchOptions).sort()
|
|
3578
3748
|
: undefined,
|
|
3579
3749
|
},
|
|
3580
|
-
...(connector.supportsSessionContinuation
|
|
3581
|
-
? {
|
|
3750
|
+
...(connector.supportsSessionContinuation || opts.supervise !== undefined
|
|
3751
|
+
? {
|
|
3752
|
+
restart: {
|
|
3753
|
+
opts: launchOpts,
|
|
3754
|
+
sessionStatePath: spec.sessionStatePath,
|
|
3755
|
+
crashes: [],
|
|
3756
|
+
recovering: false,
|
|
3757
|
+
armed: false,
|
|
3758
|
+
...(opts.supervise !== undefined ? { policy: opts.supervise } : {}),
|
|
3759
|
+
},
|
|
3760
|
+
}
|
|
3582
3761
|
: {}),
|
|
3583
3762
|
};
|
|
3584
3763
|
// Unit B: the DURABLE slot takes the `active` phase before the in-memory row takes the
|
|
@@ -3621,16 +3800,21 @@ export class Manager {
|
|
|
3621
3800
|
return { ok: false, error: readiness.detail };
|
|
3622
3801
|
}
|
|
3623
3802
|
if (managed.restart) {
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3803
|
+
if (connector.supportsSessionContinuation) {
|
|
3804
|
+
try {
|
|
3805
|
+
await this.armSessionRecovery(managed);
|
|
3806
|
+
managed.launch.sessionId = this.readManagedSession(managed);
|
|
3807
|
+
}
|
|
3808
|
+
catch (error) {
|
|
3809
|
+
const detail = `${managed.name} joined, but its exact host session could not be bound for supervised recovery: ${error.message}`;
|
|
3810
|
+
this.stopHandle(managed, false);
|
|
3811
|
+
this.freeSlot(managed, true, "session-bind-failed");
|
|
3812
|
+
await hooks?.onOutcome?.({ kind: "failed", data: { error: detail } });
|
|
3813
|
+
return { ok: false, error: detail };
|
|
3814
|
+
}
|
|
3627
3815
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
this.stopHandle(managed, false);
|
|
3631
|
-
this.freeSlot(managed, true, "session-bind-failed");
|
|
3632
|
-
await hooks?.onOutcome?.({ kind: "failed", data: { error: detail } });
|
|
3633
|
-
return { ok: false, error: detail };
|
|
3816
|
+
else {
|
|
3817
|
+
managed.restart.armed = true;
|
|
3634
3818
|
}
|
|
3635
3819
|
}
|
|
3636
3820
|
this.watchExit(managed);
|
|
@@ -4184,7 +4368,7 @@ export class Manager {
|
|
|
4184
4368
|
* `"presence"` event is only a wake; the roster is re-read as the source of truth (subscribe-then-check
|
|
4185
4369
|
* catches a join/exit that landed before we subscribed). Runtimes that stream no exit signal (external surfaces,
|
|
4186
4370
|
* whose `attach()` throws) race presence-vs-backstop only — better than the old "assume up". */
|
|
4187
|
-
async awaitReadiness(a, readinessTimeoutMs) {
|
|
4371
|
+
async awaitReadiness(a, readinessTimeoutMs, opts = {}) {
|
|
4188
4372
|
let session;
|
|
4189
4373
|
try {
|
|
4190
4374
|
session = a.handle.attach();
|
|
@@ -4206,16 +4390,25 @@ export class Manager {
|
|
|
4206
4390
|
// claims. The manager threads the uid into EVERY mode's launch (open included), so the child
|
|
4207
4391
|
// adopts it over a self-mint and publishes it in presence; the uid is absent only from a peer
|
|
4208
4392
|
// the manager never launched (a pure operator/daemon connection that never registers).
|
|
4209
|
-
|
|
4393
|
+
// A supervised restart keeps the same principal+uid, so a SIGKILL'd child's still-live
|
|
4394
|
+
// presence row would otherwise satisfy this equality. `joinedAfter` is one millisecond
|
|
4395
|
+
// past that row's last heartbeat: only a later heartbeat counts as THIS replacement joining.
|
|
4396
|
+
const joined = () => this.ep.getRoster().some((p) => p.card.id === wanted
|
|
4397
|
+
&& p.status !== "offline"
|
|
4398
|
+
&& p.lifecycleUid === a.lifecycleUid
|
|
4399
|
+
&& (opts.joinedAfter === undefined || p.ts >= opts.joinedAfter));
|
|
4210
4400
|
return await new Promise((resolve) => {
|
|
4211
4401
|
let done = false;
|
|
4212
4402
|
let timer;
|
|
4213
4403
|
let unsubExit = () => { };
|
|
4404
|
+
let joinedPoll;
|
|
4214
4405
|
const finish = (r) => {
|
|
4215
4406
|
if (done)
|
|
4216
4407
|
return;
|
|
4217
4408
|
done = true;
|
|
4218
4409
|
clearTimeout(timer);
|
|
4410
|
+
if (joinedPoll !== undefined)
|
|
4411
|
+
clearInterval(joinedPoll);
|
|
4219
4412
|
this.ep.off("presence", onPresence);
|
|
4220
4413
|
unsubExit();
|
|
4221
4414
|
resolve(r);
|
|
@@ -4224,6 +4417,11 @@ export class Manager {
|
|
|
4224
4417
|
if (joined())
|
|
4225
4418
|
finish({ ok: true });
|
|
4226
4419
|
};
|
|
4420
|
+
if (opts.joinedAfter !== undefined) {
|
|
4421
|
+
// Heartbeats bump roster.ts without emitting "presence" (same status/uid/activity).
|
|
4422
|
+
// A supervised restart needs that bump, so poll joined() rather than waiting on the event.
|
|
4423
|
+
joinedPoll = setInterval(onPresence, 50);
|
|
4424
|
+
}
|
|
4227
4425
|
// Process exit → failed. Clear the backstop FIRST (synchronously) so it can't resolve UNCERTAIN while
|
|
4228
4426
|
// the backlog reads async — the process is known dead, that's a failure, not an unknown. Reap through
|
|
4229
4427
|
// onAgentExit so a child the launcher spawned in the window is reaped too.
|
|
@@ -4255,8 +4453,14 @@ export class Manager {
|
|
|
4255
4453
|
// └──────────────────────────────────────────────────────────────────────────────────────┘
|
|
4256
4454
|
const deliberate = a.terminalizing === true;
|
|
4257
4455
|
void (async () => {
|
|
4258
|
-
|
|
4259
|
-
|
|
4456
|
+
// waitForExit may close the attach stream before this snapshot
|
|
4457
|
+
let tail = "";
|
|
4458
|
+
try {
|
|
4459
|
+
tail = this.tail(await s.backlog());
|
|
4460
|
+
}
|
|
4461
|
+
catch { }
|
|
4462
|
+
if (opts.reapOnExit !== false)
|
|
4463
|
+
this.onAgentExit(a);
|
|
4260
4464
|
// A DELIBERATE STOP IS NOT A LAUNCH FAILURE. The despawn path owns this goal's terminal
|
|
4261
4465
|
// and commits `cancel`; reporting `failed` here races it and, when it wins, tells the
|
|
4262
4466
|
// caller the agent died on launch when in fact an operator cancelled it. The process
|
|
@@ -4379,7 +4583,7 @@ export class Manager {
|
|
|
4379
4583
|
const creds = await mintCreds(this.auth, identity, "lifecycle-executor", {
|
|
4380
4584
|
lifecycleExecutor: { owner: pin.owner, actor: pin.actor, lifecycleUid: pin.lifecycleUid, alias: pin.alias },
|
|
4381
4585
|
});
|
|
4382
|
-
const nc = await
|
|
4586
|
+
const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
|
|
4383
4587
|
try {
|
|
4384
4588
|
const kvm = new Kvm(nc);
|
|
4385
4589
|
const recordsKv = await kvm.open(recordsBucket(this.space));
|
|
@@ -4404,7 +4608,7 @@ export class Manager {
|
|
|
4404
4608
|
: undefined);
|
|
4405
4609
|
if (!creds)
|
|
4406
4610
|
throw new Error("withEndpointServeExecutor: no scoped executor authority (an open mesh must use the bare path)");
|
|
4407
|
-
const nc = await
|
|
4611
|
+
const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
|
|
4408
4612
|
try {
|
|
4409
4613
|
const kvm = new Kvm(nc);
|
|
4410
4614
|
return await fn({ recordsKv: await kvm.open(recordsBucket(this.space)), authKv: await kvm.open(epAuthBucket(this.space)), nc });
|
|
@@ -4420,7 +4624,7 @@ export class Manager {
|
|
|
4420
4624
|
async withOpenServeConnection(fn) {
|
|
4421
4625
|
if (this.auth)
|
|
4422
4626
|
throw new Error("withOpenServeConnection: an auth mesh must use the scoped endpoint-serve executor");
|
|
4423
|
-
const nc = await
|
|
4627
|
+
const nc = await this.dial({ maxReconnectAttempts: 0 });
|
|
4424
4628
|
try {
|
|
4425
4629
|
const kvm = new Kvm(nc);
|
|
4426
4630
|
// An open mesh may be a RAW broker (no `cotal up` provisioning ran), and `Kvm.open` binds
|
|
@@ -4439,6 +4643,7 @@ export class Manager {
|
|
|
4439
4643
|
return {
|
|
4440
4644
|
instanceId: this.managerInstanceId,
|
|
4441
4645
|
runtime: this.runtime.kind,
|
|
4646
|
+
custody: process.platform === "linux" && this.runtime.kind === "pty" ? "custodied" : "legacy",
|
|
4442
4647
|
agentCount: this.agents.size,
|
|
4443
4648
|
uptimeMs: Date.now() - this.startedAtMs,
|
|
4444
4649
|
connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
|
|
@@ -4519,8 +4724,7 @@ export class Manager {
|
|
|
4519
4724
|
creds: remote.serveCreds,
|
|
4520
4725
|
};
|
|
4521
4726
|
const enc = new TextEncoder();
|
|
4522
|
-
const nc = await
|
|
4523
|
-
servers: this.servers ?? DEFAULT_SERVER,
|
|
4727
|
+
const nc = await this.dial({
|
|
4524
4728
|
authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce),
|
|
4525
4729
|
inboxPrefix: `_INBOX_${state.identity.id}`,
|
|
4526
4730
|
maxReconnectAttempts: -1,
|
|
@@ -4553,7 +4757,7 @@ export class Manager {
|
|
|
4553
4757
|
{
|
|
4554
4758
|
// Open mesh: the bare connection holds the rights (there is no credential system to mint from).
|
|
4555
4759
|
const provCreds = auth ? await mintCreds(auth, newIdentity(), "provisioner") : undefined;
|
|
4556
|
-
const provNc = await
|
|
4760
|
+
const provNc = await this.dial({ ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
|
|
4557
4761
|
try {
|
|
4558
4762
|
// P2 item 2: the manager now WRITES goal facts (EPF) + progress events (EPE), so the §13.12
|
|
4559
4763
|
// endpoint streams must exist. Nothing provisioned them before spawn-as-action (no endpoint
|
|
@@ -4698,8 +4902,7 @@ export class Manager {
|
|
|
4698
4902
|
// registered surface for its whole incarnation.
|
|
4699
4903
|
const state = { handle: undefined, nc: undefined, identity: serveIdentity, grant, creds };
|
|
4700
4904
|
const enc = new TextEncoder();
|
|
4701
|
-
const nc = await
|
|
4702
|
-
servers: this.servers ?? DEFAULT_SERVER,
|
|
4905
|
+
const nc = await this.dial({
|
|
4703
4906
|
// Open mesh: a bare serve connection (no credential exists; the broker enforces nothing).
|
|
4704
4907
|
...(creds !== undefined ? { authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce) } : {}),
|
|
4705
4908
|
inboxPrefix: `_INBOX_${serveIdentity.id}`,
|
|
@@ -4794,8 +4997,7 @@ export class Manager {
|
|
|
4794
4997
|
// The mutable holder captured by the authenticator (mirrors the serve connection): a half-TTL
|
|
4795
4998
|
// renewal updates `gw.creds` and the next (re)connect presents the refreshed credential.
|
|
4796
4999
|
const gw = { nc: undefined, ctx: undefined, creds: (this.auth || this.remoteAuthority) ? this.goalWriterCreds : undefined, identity };
|
|
4797
|
-
const nc = await
|
|
4798
|
-
servers: this.servers ?? DEFAULT_SERVER,
|
|
5000
|
+
const nc = await this.dial({
|
|
4799
5001
|
...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(gw.creds))(nonce) } : {}),
|
|
4800
5002
|
inboxPrefix: `_INBOX_${identity.id}`,
|
|
4801
5003
|
maxReconnectAttempts: -1,
|
|
@@ -4900,8 +5102,7 @@ export class Manager {
|
|
|
4900
5102
|
// The mutable holder captured by the authenticator (mirrors the goal-writer): a half-TTL renewal
|
|
4901
5103
|
// updates `sw.creds` and the next (re)connect presents the refreshed credential.
|
|
4902
5104
|
const sw = { nc: undefined, creds: (this.auth || this.remoteAuthority) ? this.sessionLedgerCreds : undefined };
|
|
4903
|
-
const nc = await
|
|
4904
|
-
servers: this.servers ?? DEFAULT_SERVER,
|
|
5105
|
+
const nc = await this.dial({
|
|
4905
5106
|
...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(sw.creds))(nonce) } : {}),
|
|
4906
5107
|
inboxPrefix: `_INBOX_${identity.id}`,
|
|
4907
5108
|
maxReconnectAttempts: -1,
|
|
@@ -5072,7 +5273,7 @@ export class Manager {
|
|
|
5072
5273
|
// FAIL LOUD: there is deliberately no shared connection to fall back to. Serving a session
|
|
5073
5274
|
// without its own credential is exactly the standing-writer shape this design removes.
|
|
5074
5275
|
const opts = (this.auth || this.remoteAuthority) ? standaloneConnectOpts({ creds: cred.creds, /* not yet wired to a recorded transport */ tls: false }) : {};
|
|
5075
|
-
return
|
|
5276
|
+
return this.dial({ ...opts, maxReconnectAttempts: -1 });
|
|
5076
5277
|
},
|
|
5077
5278
|
revoke: async (credentialId) => {
|
|
5078
5279
|
if (!this.auth && !this.remoteAuthority)
|
|
@@ -5127,8 +5328,8 @@ export class Manager {
|
|
|
5127
5328
|
try {
|
|
5128
5329
|
let entries = [];
|
|
5129
5330
|
const nc = this.auth
|
|
5130
|
-
? await
|
|
5131
|
-
: await
|
|
5331
|
+
? await this.dial({ ...standaloneConnectOpts({ creds: await mintCreds(this.auth, newIdentity(), "provisioner"), /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 })
|
|
5332
|
+
: await this.dial({ maxReconnectAttempts: 0 });
|
|
5132
5333
|
try {
|
|
5133
5334
|
const kvm = new Kvm(nc);
|
|
5134
5335
|
await ensureAuthorityStores(await jetstreamManager(nc), kvm, this.space);
|
|
@@ -6090,7 +6291,7 @@ export class Manager {
|
|
|
6090
6291
|
return;
|
|
6091
6292
|
const identity = newIdentity();
|
|
6092
6293
|
const creds = await mintCreds(this.auth, identity, "provisioner");
|
|
6093
|
-
const nc = await
|
|
6294
|
+
const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
|
|
6094
6295
|
const slotRows = [];
|
|
6095
6296
|
try {
|
|
6096
6297
|
const jsm = await jetstreamManager(nc);
|