@sjawhar/pi-legion-envoy 1.66.1 → 1.68.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/legion.js CHANGED
@@ -16142,7 +16142,7 @@ import { logger } from "@oh-my-pi/pi-utils";
16142
16142
  // package.json
16143
16143
  var package_default = {
16144
16144
  name: "@sjawhar/pi-legion-envoy",
16145
- version: "1.66.1",
16145
+ version: "1.68.0",
16146
16146
  type: "module",
16147
16147
  omp: {
16148
16148
  extensions: [
@@ -16155,7 +16155,7 @@ var package_default = {
16155
16155
  },
16156
16156
  legion: {
16157
16157
  daemonApiVersion: 8,
16158
- goDaemonApiVersion: 3
16158
+ goDaemonApiVersion: 4
16159
16159
  },
16160
16160
  repository: {
16161
16161
  type: "git",
@@ -31238,42 +31238,47 @@ function onEnvoyRoleRegained(listener) {
31238
31238
  }
31239
31239
 
31240
31240
  // src/legion/controller-session.ts
31241
- function createControllerSession(persistedTranscript, checkSubagentSession, rerunReadyAfterRegain, pluginVersion) {
31241
+ function typescriptControllerDaemon(rerunReadyAfterRegain, pluginVersion) {
31242
+ let daemon;
31243
+ const client = () => {
31244
+ daemon ??= createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
31245
+ return daemon;
31246
+ };
31247
+ return {
31248
+ claim: async ({ sessionID, capability, ompSessionFile, context }) => {
31249
+ const daemon2 = client();
31250
+ const { project } = await daemon2.state();
31251
+ const token = controllerToken(project);
31252
+ await claimEnvoyRole(sessionID, token, "setInterval" in context ? context : undefined);
31253
+ const ready = {
31254
+ secret: capability,
31255
+ sessionId: sessionID,
31256
+ ...ompSessionFile === undefined ? {} : { ompSessionFile },
31257
+ pluginVersion
31258
+ };
31259
+ await daemon2.controllerReady(ready);
31260
+ onEnvoyRoleRegained(async (role, reason) => {
31261
+ if (role !== token)
31262
+ return;
31263
+ await rerunReadyAfterRegain("controller/ready", role, reason, () => daemon2.controllerReady(ready));
31264
+ });
31265
+ return () => daemon2.grant({ sessionId: sessionID, secret: capability });
31266
+ }
31267
+ };
31268
+ }
31269
+ function createControllerSession(persistedTranscript, checkSubagentSession, daemon) {
31242
31270
  let controllerSessionID;
31243
31271
  let controllerCapability;
31244
- let controllerDaemon;
31245
- const controllerDaemonClient = () => {
31246
- controllerDaemon ??= createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
31247
- return controllerDaemon;
31248
- };
31272
+ let mintControllerGrant;
31249
31273
  let controllerTranscript;
31250
31274
  const claim = async (context) => {
31251
31275
  const sessionID = context.sessionManager.getSessionId();
31252
- const daemon = controllerDaemonClient();
31253
- const secret = controllerCapability ?? requiredControllerCapability(process.env);
31254
- controllerCapability = secret;
31255
- const { project } = await daemon.state();
31256
- const token = controllerToken(project);
31257
- await claimEnvoyRole(sessionID, token, "setInterval" in context ? context : undefined);
31276
+ const capability = controllerCapability ?? requiredControllerCapability(process.env);
31277
+ controllerCapability = capability;
31258
31278
  const ompSessionFile = classifySession(process.env).kind === "controller" ? (await persistedTranscript(context)).sessionFile : undefined;
31259
- await daemon.controllerReady({
31260
- secret,
31261
- sessionId: sessionID,
31262
- ...ompSessionFile === undefined ? {} : { ompSessionFile },
31263
- pluginVersion
31264
- });
31279
+ mintControllerGrant = await daemon().claim({ sessionID, capability, ompSessionFile, context });
31265
31280
  controllerSessionID = sessionID;
31266
31281
  controllerTranscript = ompSessionFile;
31267
- onEnvoyRoleRegained(async (role, reason) => {
31268
- if (role !== token)
31269
- return;
31270
- await rerunReadyAfterRegain("controller/ready", role, reason, () => daemon.controllerReady({
31271
- secret,
31272
- sessionId: sessionID,
31273
- ...controllerTranscript === undefined ? {} : { ompSessionFile: controllerTranscript },
31274
- pluginVersion
31275
- }));
31276
- });
31277
31282
  };
31278
31283
  const handleSessionStart = async (context) => {
31279
31284
  if (classifySession(process.env).kind !== "controller")
@@ -31297,13 +31302,12 @@ function createControllerSession(persistedTranscript, checkSubagentSession, reru
31297
31302
  context.ui.notify(`legion: re-claiming the controller for this session failed (${messageFor(error48)}). Until a re-claim succeeds, controller wakes and merges will not reach this session and its shell commands run without a Legion grant, so legion gh is unavailable.`, "warning");
31298
31303
  }
31299
31304
  };
31300
- const isClaimedSession = (sessionID) => controllerSessionID === sessionID && controllerCapability !== undefined;
31305
+ const isClaimedSession = (sessionID) => controllerSessionID === sessionID && mintControllerGrant !== undefined;
31301
31306
  const mintGrant = async (sessionID) => {
31302
- if (controllerSessionID !== sessionID || controllerCapability === undefined) {
31307
+ if (controllerSessionID !== sessionID || mintControllerGrant === undefined) {
31303
31308
  throw new Error("Controller session is not registered; cannot mint its grant");
31304
31309
  }
31305
- const secret = controllerCapability;
31306
- return controllerDaemonClient().grant({ sessionId: sessionID, secret });
31310
+ return mintControllerGrant();
31307
31311
  };
31308
31312
  return { claim, handleSessionStart, reclaimAfterSessionChange, isClaimedSession, mintGrant };
31309
31313
  }
@@ -31436,11 +31440,18 @@ var legionGoAdmission = exports_external.strictObject({
31436
31440
  active: exports_external.array(nonEmptyString2),
31437
31441
  waiting: exports_external.array(nonEmptyString2)
31438
31442
  });
31443
+ var legionGoControllerLocator = exports_external.strictObject({
31444
+ runtime: exports_external.enum(["tmux", "kubernetes"]),
31445
+ external: exports_external.literal(true),
31446
+ sessionId: nonEmptyString2,
31447
+ registeredAt: timestamp
31448
+ });
31439
31449
  var LegionGoStateResponse = exports_external.strictObject({
31440
31450
  daemon: goDaemonInfo,
31441
31451
  admission: legionGoAdmission,
31442
31452
  issues: exports_external.record(exports_external.string(), legionGoIssue),
31443
- pendingStatusWrites: exports_external.array(legionGoPendingStatusWrite)
31453
+ pendingStatusWrites: exports_external.array(legionGoPendingStatusWrite),
31454
+ controllerLocator: legionGoControllerLocator.optional()
31444
31455
  });
31445
31456
  var LegionGoRegisterResponse = exports_external.strictObject({
31446
31457
  claimToken: nonEmptyString2,
@@ -31450,6 +31461,15 @@ var LegionGoRegisterResponse = exports_external.strictObject({
31450
31461
  generation: exports_external.number().int().positive(),
31451
31462
  secret: nonEmptyString2
31452
31463
  });
31464
+ var LegionGoControllerRegisterResponse = exports_external.strictObject({
31465
+ claimToken: nonEmptyString2,
31466
+ role: exports_external.literal("controller"),
31467
+ generation: exports_external.number().int().positive(),
31468
+ secret: nonEmptyString2
31469
+ });
31470
+ var LegionGoControllerSecretResponse = exports_external.strictObject({
31471
+ secret: nonEmptyString2
31472
+ });
31453
31473
  var LegionGoErrorResponse = exports_external.union([
31454
31474
  exports_external.strictObject({ error: nonEmptyString2 }),
31455
31475
  exports_external.strictObject({ code: nonEmptyString2, error: nonEmptyString2 })
@@ -31504,6 +31524,10 @@ var LegionGoGrantRequest = exports_external.strictObject({
31504
31524
  tree: nonEmptyString2,
31505
31525
  issue: nonEmptyString2
31506
31526
  });
31527
+ var LegionGoControllerGrantRequest = exports_external.strictObject({
31528
+ sessionId: nonEmptyString2,
31529
+ secret: nonEmptyString2
31530
+ });
31507
31531
  var LegionGoGrantCredentialRequest = exports_external.strictObject({
31508
31532
  grantId: nonEmptyString2
31509
31533
  });
@@ -31624,9 +31648,11 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
31624
31648
  return {
31625
31649
  state: () => read("GET", "/legion/v1/state", LegionGoStateResponse),
31626
31650
  register: (input) => read("POST", "/legion/v1/claims/register", LegionGoRegisterResponse, input),
31651
+ registerController: (input) => read("POST", "/legion/v1/claims/register", LegionGoControllerRegisterResponse, input),
31627
31652
  ready: (input) => acknowledge("/legion/v1/claims/ready", input),
31628
31653
  exit: (input) => acknowledge("/legion/v1/claims/exit", input),
31629
31654
  grant: (input) => post("/legion/v1/grants", LegionGoGrantRequest, LegionGoGrantResponse, input),
31655
+ controllerGrant: (input) => post("/legion/v1/grants", LegionGoControllerGrantRequest, LegionGoGrantResponse, input),
31630
31656
  githubToken: (input) => post("/legion/v1/gh-token", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
31631
31657
  gitCredential: (input) => post("/legion/v1/git-credential", LegionGoGrantCredentialRequest, LegionGoGitCredentialResponse, input),
31632
31658
  provisioningCredential: (input) => post("/legion/v1/provisioning-credential", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
@@ -31726,13 +31752,31 @@ async function callGoReadyWithRetry(label, call) {
31726
31752
  }
31727
31753
  }
31728
31754
  }
31755
+ function goControllerDaemon(daemon, transcript) {
31756
+ return {
31757
+ claim: async ({ sessionID, capability, context }) => {
31758
+ const { sessionFile, agentId } = await transcript(context);
31759
+ const client = daemon();
31760
+ const registration = await client.registerController({
31761
+ bootToken: capability,
31762
+ sessionId: sessionID,
31763
+ ompSessionFile: sessionFile,
31764
+ agentId,
31765
+ pluginContract: package_default.legion.goDaemonApiVersion
31766
+ }).catch((error48) => {
31767
+ if (error48 instanceof LegionGoDaemonApiError) {
31768
+ console.error(`[legion] claims/register for the controller failed (${error48.status}): ${error48.detail}`);
31769
+ }
31770
+ throw error48;
31771
+ });
31772
+ await claimEnvoyRole(sessionID, registration.claimToken, "setInterval" in context ? context : undefined);
31773
+ return () => client.controllerGrant({ sessionId: sessionID, secret: registration.secret });
31774
+ }
31775
+ };
31776
+ }
31729
31777
  async function bootstrapGoClaim(context, state) {
31730
- const classification = classifySession(process.env);
31731
- if (classification.kind === "not-legion")
31778
+ if (classifySession(process.env).kind === "not-legion")
31732
31779
  return;
31733
- if (classification.kind === "controller") {
31734
- throw new Error("LEGION_DAEMON_API=go names the Go daemon, which launches no controller session");
31735
- }
31736
31780
  const sessionID = context.sessionManager.getSessionId();
31737
31781
  const capability = state.capability();
31738
31782
  if (capability !== undefined && capability.sessionID !== sessionID)
@@ -32626,11 +32670,13 @@ function legionExtension(pi) {
32626
32670
  console.error(`[legion] ${endpoint} after role ${role} was ${reason} failed; the daemon's held work stays undelivered until the next regain or boot: ${messageFor(error48)}`);
32627
32671
  }
32628
32672
  };
32673
+ const typescriptController = typescriptControllerDaemon(rerunReadyAfterRegain, package_default.version);
32674
+ const goController = goControllerDaemon(goRoleDaemon, persistedTranscript);
32629
32675
  const controllerSession = createControllerSession(async (context) => {
32630
32676
  const persisted = await persistedTranscript(context);
32631
32677
  recordBootstrappedSession(persisted.sessionFile);
32632
32678
  return persisted;
32633
- }, checkSubagentSession, rerunReadyAfterRegain, package_default.version);
32679
+ }, checkSubagentSession, () => process.env.LEGION_DAEMON_API === "go" ? goController : typescriptController);
32634
32680
  const reclaimArchitect = async () => {
32635
32681
  if (capability === undefined || capability.kind !== "root-architect") {
32636
32682
  throw new Error("Legion root architect is not available for reclamation");
@@ -32828,6 +32874,10 @@ function legionExtension(pi) {
32828
32874
  return;
32829
32875
  phaseStall = restorePhaseStall(context.sessionManager.getBranch?.() ?? []);
32830
32876
  if (process.env.LEGION_DAEMON_API === "go") {
32877
+ if (classifySession(process.env).kind === "controller") {
32878
+ await controllerSession.handleSessionStart(context);
32879
+ return;
32880
+ }
32831
32881
  await bootstrapGoClaim(context, {
32832
32882
  capability: () => capability,
32833
32883
  setCapability: (next) => {
@@ -59,23 +59,26 @@ command run `/legion-claim-controller` again.
59
59
  This handshake lets the daemon redeliver held controller work. It does not turn the controller
60
60
  into a state holder: daemon state and the Dispatch project remain authoritative.
61
61
 
62
- ### Started by the operator (runtime: kubernetes)
63
-
64
- When the daemon runs inside a Kubernetes cluster it cannot open a terminal anywhere, so nobody
65
- launched your pane: the operator ran `legion controller start --config controller.yaml
66
- [--daemon-url <port-forward>]` on their own machine, and you are that foreground OMP session.
67
- The command fetched a fresh controller secret from the daemon with the operator's token, wrote it
68
- to a 0600 file under `LEGION_STATE_DIR` (`~/.local/state/legion/<project>-controller` by default)
69
- beside the `gh` shim and the `legion` launcher, and started you with `LEGION_CONTROLLER=1` and
70
- the same environment a tmux controller pane carries — so the extension claims the role and calls
71
- `/controller/ready` exactly as under tmux, and nothing changes in how you handle wakes. The
72
- daemon records you as `controllerLocator: {runtime: "kubernetes", external: true, sessionId,
73
- registeredAt}` and reads your liveness from the Envoy role registry (the holder of
62
+ ### Started by the operator
63
+
64
+ When the daemon cannot open a terminal for you — the TypeScript daemon under `runtime: kubernetes`,
65
+ and the Go daemon under either runtime, since it launches no controller — nobody launched your
66
+ pane: the operator ran `legion controller start --config controller.yaml [--daemon-url <url>]` on
67
+ their own machine, and you are that foreground OMP session. The command fetched a fresh controller
68
+ secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
69
+ (`~/.local/state/legion/<project>-controller` by default) beside the `gh` shim and the `legion`
70
+ launcher, and started you with `LEGION_CONTROLLER=1` and the same environment a tmux controller pane
71
+ carries, so nothing changes in how you handle wakes. Under the TypeScript daemon the extension
72
+ claims the role and calls `/controller/ready` exactly as under tmux; under the Go daemon
73
+ (`LEGION_DAEMON_API=go` in your environment) it registers on `/legion/v1/claims/register` with the
74
+ secret, then claims the role. The daemon records you as `controllerLocator: {runtime, external:
75
+ true, sessionId, registeredAt}`, `runtime` being the daemon's own (`kubernetes`, or `tmux` under the
76
+ Go daemon). The TypeScript daemon reads your liveness from the Envoy role registry (the holder of
74
77
  `legion-<project>-controller` and its `last_seen`), not from a pane: keep the session running.
75
78
  Exiting it leaves the project without a controller until the operator runs the command again —
76
- the daemon logs `controller not registered; run legion controller start` once per boot-timeout
77
- interval and launches nothing itself. `legion state` and `legion status <KEY> <status>` work here
78
- over `LEGION_DAEMON_URL` (the port-forward). A second `legion controller start` replaces you: it
79
+ the TypeScript daemon logs `controller not registered; run legion controller start` once per
80
+ boot-timeout interval and launches nothing itself. `legion state` and `legion status <KEY>
81
+ <status>` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
79
82
  mints a new secret, so your grants stop working and the role moves to the new session.
80
83
 
81
84
  ## Deployment instructions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.66.1",
3
+ "version": "1.68.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "legion": {
15
15
  "daemonApiVersion": 8,
16
- "goDaemonApiVersion": 3
16
+ "goDaemonApiVersion": 4
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",