@sjawhar/pi-legion-envoy 1.67.0 → 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 +91 -41
- package/dist/skills/legion-controller/SKILL.md +18 -15
- package/package.json +2 -2
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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
31253
|
-
|
|
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.
|
|
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 &&
|
|
31305
|
+
const isClaimedSession = (sessionID) => controllerSessionID === sessionID && mintControllerGrant !== undefined;
|
|
31301
31306
|
const mintGrant = async (sessionID) => {
|
|
31302
|
-
if (controllerSessionID !== sessionID ||
|
|
31307
|
+
if (controllerSessionID !== sessionID || mintControllerGrant === undefined) {
|
|
31303
31308
|
throw new Error("Controller session is not registered; cannot mint its grant");
|
|
31304
31309
|
}
|
|
31305
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
63
|
-
|
|
64
|
-
When the daemon
|
|
65
|
-
|
|
66
|
-
[--daemon-url <
|
|
67
|
-
|
|
68
|
-
to a 0600 file under `LEGION_STATE_DIR`
|
|
69
|
-
beside the `gh` shim and the `legion`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
77
|
-
interval and launches nothing itself. `legion state` and `legion status <KEY>
|
|
78
|
-
over `LEGION_DAEMON_URL
|
|
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.
|
|
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":
|
|
16
|
+
"goDaemonApiVersion": 4
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|