create-egregore 0.21.0-runtime-mvp.21 → 0.21.0-runtime-mvp.23
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/assets/egregore-launcher.js +44 -12
- package/lib/setup.js +50 -13
- package/package.json +1 -1
- package/runtime/claude/bin/runtime-identity.sh +52 -14
- package/runtime/claude/bin/runtime-upgrade.sh +28 -6
- package/runtime/claude/egregore_runtime/identity_cli.py +55 -0
- package/runtime/codex/bin/runtime-identity.sh +52 -14
- package/runtime/codex/bin/runtime-upgrade.sh +28 -6
- package/runtime/codex/egregore_runtime/identity_cli.py +55 -0
- package/runtime/pi/bin/runtime-identity.sh +52 -14
- package/runtime/pi/bin/runtime-upgrade.sh +28 -6
- package/runtime/pi/egregore_runtime/identity_cli.py +55 -0
- package/runtime/prime/bin/runtime-identity.sh +52 -14
- package/runtime/prime/bin/runtime-upgrade.sh +28 -6
- package/runtime/prime/egregore_runtime/identity_cli.py +55 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
2
|
+
// egregore-launcher-version: 0.21.0-runtime-mvp.23
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.0-runtime-mvp.
|
|
38
|
+
const VERSION = "0.21.0-runtime-mvp.23";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -285,7 +285,23 @@ function readRegistry() {
|
|
|
285
285
|
if (key.includes("/.claude/worktrees/")) continue;
|
|
286
286
|
if (seen.has(key)) continue;
|
|
287
287
|
seen.add(key);
|
|
288
|
-
|
|
288
|
+
// The checkout config is the canonical identity for a path. Older
|
|
289
|
+
// installers could overwrite registry labels when two organizations
|
|
290
|
+
// used the same repository/folder name, producing hybrid rows such as
|
|
291
|
+
// another org's slug beside the Curve Labs checkout.
|
|
292
|
+
let canonical = e;
|
|
293
|
+
try {
|
|
294
|
+
const config = JSON.parse(
|
|
295
|
+
fs.readFileSync(path.join(key, "egregore.json"), "utf-8")
|
|
296
|
+
);
|
|
297
|
+
canonical = {
|
|
298
|
+
...e,
|
|
299
|
+
slug: config.slug || e.slug,
|
|
300
|
+
name: config.org_name || e.name,
|
|
301
|
+
};
|
|
302
|
+
if (config.org_id) canonical.org_id = config.org_id;
|
|
303
|
+
} catch {}
|
|
304
|
+
result.push(canonical);
|
|
289
305
|
}
|
|
290
306
|
return result;
|
|
291
307
|
} catch {
|
|
@@ -2499,20 +2515,33 @@ function formatTelemetryLines(status) {
|
|
|
2499
2515
|
function upgradeStatus(inst, { fetch = false } = {}) {
|
|
2500
2516
|
const args = ["status"]; if (fetch) args.push("--fetch");
|
|
2501
2517
|
const r = runUpgrade(inst, args);
|
|
2502
|
-
if (!r || r.status !== 0 || !r.stdout)
|
|
2503
|
-
|
|
2518
|
+
if (!r || r.status !== 0 || !r.stdout) {
|
|
2519
|
+
const detail = String((r && (r.stderr || r.stdout)) || "Runtime status probe failed")
|
|
2520
|
+
.trim().split("\n")[0];
|
|
2521
|
+
return { status: "unavailable", failure: { message: detail } };
|
|
2522
|
+
}
|
|
2523
|
+
try { return JSON.parse(r.stdout); } catch {
|
|
2524
|
+
return { status: "unavailable", failure: { message: "Runtime status returned an unreadable response" } };
|
|
2525
|
+
}
|
|
2504
2526
|
}
|
|
2505
2527
|
|
|
2506
2528
|
function upgradeStatusAsync(inst, cb) {
|
|
2507
2529
|
const bridge = resolveUpgradeBridge(inst);
|
|
2508
|
-
if (!bridge) return cb(
|
|
2509
|
-
let buf = "";
|
|
2530
|
+
if (!bridge) return cb({ status: "unavailable", failure: { message: "No Runtime update engine is installed" } });
|
|
2531
|
+
let buf = ""; let err = "";
|
|
2510
2532
|
try {
|
|
2511
|
-
const child = spawn("bash", [bridge.script, "status"], { cwd: inst.path, stdio: ["ignore", "pipe", "
|
|
2533
|
+
const child = spawn("bash", [bridge.script, "status"], { cwd: inst.path, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...bridge.env } });
|
|
2512
2534
|
child.stdout.on("data", (d) => { buf += d; });
|
|
2513
|
-
child.on("
|
|
2514
|
-
child.on("
|
|
2515
|
-
|
|
2535
|
+
child.stderr.on("data", (d) => { err += d; });
|
|
2536
|
+
child.on("close", (code) => {
|
|
2537
|
+
if (code === 0) {
|
|
2538
|
+
try { cb(JSON.parse(buf)); return; } catch {}
|
|
2539
|
+
}
|
|
2540
|
+
const detail = String(err || buf || "Runtime status probe failed").trim().split("\n")[0];
|
|
2541
|
+
cb({ status: "unavailable", failure: { message: detail } });
|
|
2542
|
+
});
|
|
2543
|
+
child.on("error", (error) => cb({ status: "unavailable", failure: { message: error.message } }));
|
|
2544
|
+
} catch (error) { cb({ status: "unavailable", failure: { message: error.message } }); }
|
|
2516
2545
|
}
|
|
2517
2546
|
|
|
2518
2547
|
// Local retrieval health for the settings surface. Package-update state,
|
|
@@ -2632,6 +2661,7 @@ const UPGRADE_STATUS_LABEL = {
|
|
|
2632
2661
|
active: "active",
|
|
2633
2662
|
cancelled: "cancelled",
|
|
2634
2663
|
"rolled-back": "rolled back",
|
|
2664
|
+
unavailable: "unavailable",
|
|
2635
2665
|
};
|
|
2636
2666
|
|
|
2637
2667
|
// Settings-row truth for the Runtime update line. The row must say outright
|
|
@@ -2645,6 +2675,7 @@ function runtimeUpdateRowValue(st, candidate) {
|
|
|
2645
2675
|
if (candidate && candidate !== v) return `${v} active ${YELLOW}· update available ${candidate}${RESET}`;
|
|
2646
2676
|
return `${GREEN}${v} active${RESET}${candidate ? `${DIM} · up to date${RESET}` : ""}`;
|
|
2647
2677
|
}
|
|
2678
|
+
if (st.status === "unavailable") return `${YELLOW}update unavailable${RESET}`;
|
|
2648
2679
|
if (st.status === "ready-to-activate") return `${GREEN}${upgradeSummary(st)}${RESET}`;
|
|
2649
2680
|
if (st.status === "failed" || st.status === "interrupted") return `${YELLOW}${upgradeSummary(st)}${RESET}`;
|
|
2650
2681
|
if (st.status === "preparing" || st.status === "pending") return `${DIM}${upgradeSummary(st)}${RESET}`;
|
|
@@ -2685,7 +2716,7 @@ function formatUpgradeLines(inst, st, { candidate = "", retrieval = null } = {})
|
|
|
2685
2716
|
if (d.candidate_digest) lines.push(`Package digest: sha256 ${String(d.candidate_digest).slice(0, 16)}…`);
|
|
2686
2717
|
// A recorded failure only needs attention while the transaction is stuck
|
|
2687
2718
|
// on it; once a later prepare succeeds the line would contradict "ready".
|
|
2688
|
-
if (d.failure && d.failure.message && (
|
|
2719
|
+
if (d.failure && d.failure.message && (["failed", "interrupted", "unavailable"].includes(d.status))) {
|
|
2689
2720
|
lines.push(`Attention: ${d.failure.message}`);
|
|
2690
2721
|
}
|
|
2691
2722
|
if (d.rollback_version && d.status !== "none") lines.push(`Rollback target: ${d.rollback_version}`);
|
|
@@ -2760,6 +2791,7 @@ async function cmdSettingsRuntime(inst, args) {
|
|
|
2760
2791
|
const st = upgradeStatus(inst, { fetch: fetchFlag });
|
|
2761
2792
|
if (st === null) { process.stderr.write("Runtime update state is unavailable for this egregore.\n"); process.exitCode = 1; return; }
|
|
2762
2793
|
emit(st);
|
|
2794
|
+
if (st.status === "unavailable") process.exitCode = 1;
|
|
2763
2795
|
if (action === "review" || (st.status !== "active" && st.status !== "none")) {
|
|
2764
2796
|
process.stdout.write("\nWhat stays:\n");
|
|
2765
2797
|
for (const s of UPGRADE_STAYS) process.stdout.write(` · ${s}\n`);
|
package/lib/setup.js
CHANGED
|
@@ -219,17 +219,37 @@ function slugifyName(name) {
|
|
|
219
219
|
*
|
|
220
220
|
* Most Egregore forks are named "egregore", so deriving the target only from
|
|
221
221
|
* the fork basename makes different organizations collide in the same parent
|
|
222
|
-
* directory. If that default path
|
|
223
|
-
* origin, leave it untouched and use egregore-{org-slug}. The fallback
|
|
224
|
-
* also be absent or
|
|
225
|
-
* guessing at another path.
|
|
222
|
+
* directory. If that default path already belongs to another configured org
|
|
223
|
+
* or Git origin, leave it untouched and use egregore-{org-slug}. The fallback
|
|
224
|
+
* must also be absent or belong to the requested org and fork; otherwise stop
|
|
225
|
+
* rather than guessing at another path.
|
|
226
226
|
*/
|
|
227
|
-
function
|
|
227
|
+
function configuredIdentityConflicts(dir, slug, orgId = "") {
|
|
228
|
+
let config;
|
|
229
|
+
try {
|
|
230
|
+
config = JSON.parse(fs.readFileSync(path.join(dir, "egregore.json"), "utf-8"));
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const existingSlug = slugifyName(config.slug || "");
|
|
235
|
+
const requestedSlug = slugifyName(slug || "");
|
|
236
|
+
if (existingSlug && requestedSlug && existingSlug !== requestedSlug) return true;
|
|
237
|
+
|
|
238
|
+
const stable = (value) => typeof value === "string" && value && !value.startsWith("legacy-org:");
|
|
239
|
+
return stable(config.org_id) && stable(orgId) && config.org_id !== orgId;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function installTargetMatches(dir, forkUrl, slug, orgId = "") {
|
|
243
|
+
if (configuredIdentityConflicts(dir, slug, orgId)) return false;
|
|
244
|
+
const gitPath = path.join(dir, ".git");
|
|
245
|
+
return !fs.existsSync(gitPath) || gitRemoteMatches(dir, forkUrl);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveEgregoreInstallDir(base, forkUrl, slug, orgId = "") {
|
|
228
249
|
const forkDirName = forkUrl.split("/").pop().replace(/\.git$/, "");
|
|
229
250
|
const requestedDir = path.join(base, forkDirName);
|
|
230
|
-
const requestedGitPath = path.join(requestedDir, ".git");
|
|
231
251
|
|
|
232
|
-
if (
|
|
252
|
+
if (installTargetMatches(requestedDir, forkUrl, slug, orgId)) {
|
|
233
253
|
return { egregoreDir: requestedDir, redirectedFrom: null };
|
|
234
254
|
}
|
|
235
255
|
|
|
@@ -237,15 +257,14 @@ function resolveEgregoreInstallDir(base, forkUrl, slug) {
|
|
|
237
257
|
const fallbackDir = path.join(base, `egregore-${safeSlug}`);
|
|
238
258
|
if (path.resolve(fallbackDir) === path.resolve(requestedDir)) {
|
|
239
259
|
throw new Error(
|
|
240
|
-
`Refusing to install into ${requestedDir}:
|
|
260
|
+
`Refusing to install into ${requestedDir}: it belongs to another Egregore or Git origin`
|
|
241
261
|
);
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
if (fs.existsSync(fallbackDir)) {
|
|
245
|
-
|
|
246
|
-
if (!fs.existsSync(fallbackGitPath) || !gitRemoteMatches(fallbackDir, forkUrl)) {
|
|
265
|
+
if (!installTargetMatches(fallbackDir, forkUrl, slug, orgId)) {
|
|
247
266
|
throw new Error(
|
|
248
|
-
`Refusing to install into ${fallbackDir}: the fallback path already belongs to another checkout or directory`
|
|
267
|
+
`Refusing to install into ${fallbackDir}: the fallback path already belongs to another Egregore, checkout, or directory`
|
|
249
268
|
);
|
|
250
269
|
}
|
|
251
270
|
}
|
|
@@ -444,11 +463,16 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
444
463
|
const {
|
|
445
464
|
egregoreDir,
|
|
446
465
|
redirectedFrom,
|
|
447
|
-
} = resolveEgregoreInstallDir(
|
|
466
|
+
} = resolveEgregoreInstallDir(
|
|
467
|
+
base,
|
|
468
|
+
fork_url,
|
|
469
|
+
slug || github_org || org_name,
|
|
470
|
+
data.org_id || "",
|
|
471
|
+
);
|
|
448
472
|
if (redirectedFrom) {
|
|
449
473
|
forkDirName = path.basename(egregoreDir);
|
|
450
474
|
ui.warn(
|
|
451
|
-
`${redirectedFrom} belongs to another Git origin — leaving it untouched and installing at ${egregoreDir}`
|
|
475
|
+
`${redirectedFrom} belongs to another Egregore or Git origin — leaving it untouched and installing at ${egregoreDir}`
|
|
452
476
|
);
|
|
453
477
|
}
|
|
454
478
|
const memoryDirName = memory_url
|
|
@@ -1176,10 +1200,23 @@ function registerInstance(slug, name, egregoreDir, runtime) {
|
|
|
1176
1200
|
}
|
|
1177
1201
|
}
|
|
1178
1202
|
|
|
1203
|
+
// The checkout config is canonical. A historic same-path install could pass
|
|
1204
|
+
// another organization's label here and splice that label onto this path.
|
|
1205
|
+
let canonicalOrgId = "";
|
|
1206
|
+
try {
|
|
1207
|
+
const config = JSON.parse(
|
|
1208
|
+
fs.readFileSync(path.join(egregoreDir, "egregore.json"), "utf-8")
|
|
1209
|
+
);
|
|
1210
|
+
slug = config.slug || slug;
|
|
1211
|
+
name = config.org_name || name;
|
|
1212
|
+
canonicalOrgId = config.org_id || "";
|
|
1213
|
+
} catch {}
|
|
1214
|
+
|
|
1179
1215
|
// Dedup by path (not slug) so multiple instances from the same org can coexist
|
|
1180
1216
|
const idx = instances.findIndex((i) => i.path === egregoreDir);
|
|
1181
1217
|
const previous = idx >= 0 ? instances[idx] : {};
|
|
1182
1218
|
const entry = { ...previous, slug, name, path: egregoreDir };
|
|
1219
|
+
if (canonicalOrgId) entry.org_id = canonicalOrgId;
|
|
1183
1220
|
if (runtime) {
|
|
1184
1221
|
entry.runtime = normalizeRuntime(runtime);
|
|
1185
1222
|
} else if (!entry.runtime && entry.preferred_runtime) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.21.0-runtime-mvp.
|
|
3
|
+
"version": "0.21.0-runtime-mvp.23",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -6,7 +6,12 @@ set -euo pipefail
|
|
|
6
6
|
# legacy Local instances create opaque local IDs once. Network failure remains
|
|
7
7
|
# non-fatal to startup and is retried next session.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
RUNTIME_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
# A launcher-carried bridge runs outside the selected instance. Keep code
|
|
11
|
+
# rooted in the packaged Runtime while resolving and persisting identity in
|
|
12
|
+
# exactly the instance the launcher selected.
|
|
13
|
+
ROOT="${EGREGORE_INSTANCE_ROOT:-$RUNTIME_ROOT}"
|
|
14
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
10
15
|
CONFIG="$ROOT/egregore.json"
|
|
11
16
|
STATE="$ROOT/.egregore-state.json"
|
|
12
17
|
|
|
@@ -34,21 +39,52 @@ case "${1:-ensure}" in
|
|
|
34
39
|
resolved && exit 0
|
|
35
40
|
mode="$(jq -r 'if (.mode // "local") == "connected" or (.api_url // "") != "" then "connected" else "local" end' "$CONFIG" 2>/dev/null || echo local)"
|
|
36
41
|
if [ "$mode" = "local" ]; then
|
|
37
|
-
EGREGORE_ROOT="$ROOT"
|
|
42
|
+
EGREGORE_ROOT="$ROOT" \
|
|
43
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
44
|
+
python3 -m egregore_runtime.identity_cli initialize-local >/dev/null
|
|
38
45
|
else
|
|
39
46
|
api_url="$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)"
|
|
40
|
-
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
41
|
-
[ -n "$api_url" ] && [ -n "$api_key" ] || exit 1
|
|
42
|
-
# Identity repair is read-only: select the already-existing account and
|
|
43
|
-
# membership records, then adopt their opaque IDs locally. If migration
|
|
44
|
-
# 038 has not reached the control plane, the next session retries.
|
|
45
|
-
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
46
|
-
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
47
|
-
2>/dev/null || true)"
|
|
48
|
-
printf '%s' "$control_plane" | jq -e \
|
|
49
|
-
'.org_id | type == "string" and length > 0' >/dev/null 2>&1 || exit 1
|
|
50
47
|
slug="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
|
|
51
|
-
|
|
48
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
49
|
+
[ -n "$api_url" ] && [ -n "$slug" ] || exit 1
|
|
50
|
+
# Identity repair selects already-existing account and membership
|
|
51
|
+
# records, then adopts their opaque IDs locally. If a stale credential
|
|
52
|
+
# belongs to another org, recover only the configured org's existing key
|
|
53
|
+
# through verified GitHub membership. No control-plane records are made.
|
|
54
|
+
control_plane=""
|
|
55
|
+
if [ -n "$api_key" ]; then
|
|
56
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
57
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
58
|
+
2>/dev/null || true)"
|
|
59
|
+
fi
|
|
60
|
+
if ! printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
61
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
62
|
+
' >/dev/null 2>&1; then
|
|
63
|
+
# A historic path/namespace collision could leave this checkout with
|
|
64
|
+
# another Egregore's API key. Recover the configured org's existing key
|
|
65
|
+
# only through the membership-verifying endpoint, install it atomically,
|
|
66
|
+
# and retry. No credential bytes are printed.
|
|
67
|
+
github_token="$(grep '^GITHUB_TOKEN=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
68
|
+
[ -n "$github_token" ] || exit 1
|
|
69
|
+
encoded_slug="$(printf '%s' "$slug" | jq -sRr @uri)"
|
|
70
|
+
recovered="$(curl -sf "$api_url/api/org/$encoded_slug/key" \
|
|
71
|
+
-H "Authorization: Bearer $github_token" --connect-timeout 2 --max-time 12 \
|
|
72
|
+
2>/dev/null || true)"
|
|
73
|
+
printf '%s' "$recovered" | jq -e --arg slug "$slug" '
|
|
74
|
+
(.api_key | type == "string" and length > 0) and .org_slug == $slug
|
|
75
|
+
' >/dev/null 2>&1 || exit 1
|
|
76
|
+
printf '%s' "$recovered" | \
|
|
77
|
+
EGREGORE_ROOT="$ROOT" \
|
|
78
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
79
|
+
python3 -m egregore_runtime.identity_cli install-connected-key >/dev/null
|
|
80
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
81
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
82
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
83
|
+
2>/dev/null || true)"
|
|
84
|
+
printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
85
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
86
|
+
' >/dev/null 2>&1 || exit 1
|
|
87
|
+
fi
|
|
52
88
|
roster="$(curl -sf "$api_url/api/org/$slug/members?include_removed=true" \
|
|
53
89
|
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
54
90
|
2>/dev/null || true)"
|
|
@@ -58,7 +94,9 @@ case "${1:-ensure}" in
|
|
|
58
94
|
--argjson organization "$control_plane" \
|
|
59
95
|
--argjson roster "$roster" \
|
|
60
96
|
'{organization:$organization,roster:$roster}' | \
|
|
61
|
-
EGREGORE_ROOT="$ROOT"
|
|
97
|
+
EGREGORE_ROOT="$ROOT" \
|
|
98
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
99
|
+
python3 -m egregore_runtime.identity_cli \
|
|
62
100
|
reconcile-records >/dev/null
|
|
63
101
|
fi
|
|
64
102
|
resolved
|
|
@@ -28,13 +28,35 @@ _runtime() {
|
|
|
28
28
|
python3 -m egregore_runtime.harness_cli "$@"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
_ensure_identity() {
|
|
32
|
+
# Pre-Runtime instances can predate stable organization identity. Resolve it
|
|
33
|
+
# through the existing verified Local/Connected migration before computing
|
|
34
|
+
# the instance-scoped upgrade key; never infer identity from path or slug.
|
|
35
|
+
if jq -e '
|
|
36
|
+
.org_id | type == "string" and length > 0 and (startswith("legacy-org:") | not)
|
|
37
|
+
' "$INSTANCE_ROOT/egregore.json" >/dev/null 2>&1; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
EGREGORE_INSTANCE_ROOT="$INSTANCE_ROOT" \
|
|
41
|
+
PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
|
|
42
|
+
bash "$SCRIPT_DIR/bin/runtime-identity.sh" ensure
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_run_upgrade() {
|
|
46
|
+
_ensure_identity || {
|
|
47
|
+
echo "Runtime update could not resolve this Egregore's stable identity." >&2
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
_runtime upgrade "$@"
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
case "${1:-}" in
|
|
32
|
-
status) shift;
|
|
33
|
-
init) shift;
|
|
34
|
-
prepare) shift;
|
|
35
|
-
activate) shift;
|
|
36
|
-
cancel) shift;
|
|
37
|
-
rollback) shift;
|
|
54
|
+
status) shift; _run_upgrade status "$@" ;;
|
|
55
|
+
init) shift; _run_upgrade init "$@" ;;
|
|
56
|
+
prepare) shift; _run_upgrade prepare "$@" ;;
|
|
57
|
+
activate) shift; _run_upgrade activate "$@" ;;
|
|
58
|
+
cancel) shift; _run_upgrade cancel "$@" ;;
|
|
59
|
+
rollback) shift; _run_upgrade rollback "$@" ;;
|
|
38
60
|
team-sync) shift; _runtime team-sync "$@" ;;
|
|
39
61
|
*)
|
|
40
62
|
echo "usage: bash bin/runtime-upgrade.sh {status|init --candidate <version>|prepare|activate|cancel|rollback|team-sync}" >&2
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from .contracts import Permission
|
|
@@ -44,6 +45,55 @@ def _authorize_self(root: Path, permission: Permission, resource: str) -> int:
|
|
|
44
45
|
return 0
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def _install_connected_key(root: Path, payload: dict) -> dict[str, str]:
|
|
49
|
+
"""Install a membership-verified key without exposing it in output."""
|
|
50
|
+
|
|
51
|
+
config = json.loads((root / "egregore.json").read_text(encoding="utf-8"))
|
|
52
|
+
expected_slug = str(config.get("slug") or "").strip()
|
|
53
|
+
returned_slug = str(payload.get("org_slug") or "").strip()
|
|
54
|
+
api_key = str(payload.get("api_key") or "").strip()
|
|
55
|
+
if not expected_slug or returned_slug != expected_slug:
|
|
56
|
+
raise ValueError("recovered credential does not match the configured organization")
|
|
57
|
+
if not api_key or "\n" in api_key or "\r" in api_key:
|
|
58
|
+
raise ValueError("recovered credential is missing or malformed")
|
|
59
|
+
|
|
60
|
+
env_path = root / ".env"
|
|
61
|
+
existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
62
|
+
output: list[str] = []
|
|
63
|
+
replaced = False
|
|
64
|
+
for line in existing.splitlines():
|
|
65
|
+
if line.startswith("EGREGORE_API_KEY="):
|
|
66
|
+
if not replaced:
|
|
67
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
68
|
+
replaced = True
|
|
69
|
+
continue
|
|
70
|
+
output.append(line)
|
|
71
|
+
if not replaced:
|
|
72
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
73
|
+
|
|
74
|
+
temporary: Path | None = None
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.NamedTemporaryFile(
|
|
77
|
+
mode="w",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
dir=root,
|
|
80
|
+
prefix=".env.egregore-key.",
|
|
81
|
+
delete=False,
|
|
82
|
+
) as handle:
|
|
83
|
+
temporary = Path(handle.name)
|
|
84
|
+
os.chmod(temporary, 0o600)
|
|
85
|
+
handle.write("\n".join(output) + "\n")
|
|
86
|
+
handle.flush()
|
|
87
|
+
os.fsync(handle.fileno())
|
|
88
|
+
os.replace(temporary, env_path)
|
|
89
|
+
temporary = None
|
|
90
|
+
finally:
|
|
91
|
+
if temporary is not None:
|
|
92
|
+
temporary.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
return {"org_slug": expected_slug}
|
|
95
|
+
|
|
96
|
+
|
|
47
97
|
def main() -> int:
|
|
48
98
|
root = Path(os.environ.get("EGREGORE_ROOT") or Path.cwd()).resolve()
|
|
49
99
|
if len(sys.argv) > 1 and sys.argv[1] == "initialize-local":
|
|
@@ -77,6 +127,11 @@ def main() -> int:
|
|
|
77
127
|
result = reconcile_control_plane_identity(root, response)
|
|
78
128
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
79
129
|
return 0
|
|
130
|
+
if len(sys.argv) > 1 and sys.argv[1] == "install-connected-key":
|
|
131
|
+
payload = json.load(sys.stdin)
|
|
132
|
+
result = _install_connected_key(root, payload)
|
|
133
|
+
print(json.dumps({"status": "installed", **result}, sort_keys=True))
|
|
134
|
+
return 0
|
|
80
135
|
payload = json.load(sys.stdin)
|
|
81
136
|
result = reconcile_control_plane_identity(root, payload)
|
|
82
137
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
@@ -6,7 +6,12 @@ set -euo pipefail
|
|
|
6
6
|
# legacy Local instances create opaque local IDs once. Network failure remains
|
|
7
7
|
# non-fatal to startup and is retried next session.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
RUNTIME_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
# A launcher-carried bridge runs outside the selected instance. Keep code
|
|
11
|
+
# rooted in the packaged Runtime while resolving and persisting identity in
|
|
12
|
+
# exactly the instance the launcher selected.
|
|
13
|
+
ROOT="${EGREGORE_INSTANCE_ROOT:-$RUNTIME_ROOT}"
|
|
14
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
10
15
|
CONFIG="$ROOT/egregore.json"
|
|
11
16
|
STATE="$ROOT/.egregore-state.json"
|
|
12
17
|
|
|
@@ -34,21 +39,52 @@ case "${1:-ensure}" in
|
|
|
34
39
|
resolved && exit 0
|
|
35
40
|
mode="$(jq -r 'if (.mode // "local") == "connected" or (.api_url // "") != "" then "connected" else "local" end' "$CONFIG" 2>/dev/null || echo local)"
|
|
36
41
|
if [ "$mode" = "local" ]; then
|
|
37
|
-
EGREGORE_ROOT="$ROOT"
|
|
42
|
+
EGREGORE_ROOT="$ROOT" \
|
|
43
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
44
|
+
python3 -m egregore_runtime.identity_cli initialize-local >/dev/null
|
|
38
45
|
else
|
|
39
46
|
api_url="$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)"
|
|
40
|
-
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
41
|
-
[ -n "$api_url" ] && [ -n "$api_key" ] || exit 1
|
|
42
|
-
# Identity repair is read-only: select the already-existing account and
|
|
43
|
-
# membership records, then adopt their opaque IDs locally. If migration
|
|
44
|
-
# 038 has not reached the control plane, the next session retries.
|
|
45
|
-
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
46
|
-
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
47
|
-
2>/dev/null || true)"
|
|
48
|
-
printf '%s' "$control_plane" | jq -e \
|
|
49
|
-
'.org_id | type == "string" and length > 0' >/dev/null 2>&1 || exit 1
|
|
50
47
|
slug="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
|
|
51
|
-
|
|
48
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
49
|
+
[ -n "$api_url" ] && [ -n "$slug" ] || exit 1
|
|
50
|
+
# Identity repair selects already-existing account and membership
|
|
51
|
+
# records, then adopts their opaque IDs locally. If a stale credential
|
|
52
|
+
# belongs to another org, recover only the configured org's existing key
|
|
53
|
+
# through verified GitHub membership. No control-plane records are made.
|
|
54
|
+
control_plane=""
|
|
55
|
+
if [ -n "$api_key" ]; then
|
|
56
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
57
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
58
|
+
2>/dev/null || true)"
|
|
59
|
+
fi
|
|
60
|
+
if ! printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
61
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
62
|
+
' >/dev/null 2>&1; then
|
|
63
|
+
# A historic path/namespace collision could leave this checkout with
|
|
64
|
+
# another Egregore's API key. Recover the configured org's existing key
|
|
65
|
+
# only through the membership-verifying endpoint, install it atomically,
|
|
66
|
+
# and retry. No credential bytes are printed.
|
|
67
|
+
github_token="$(grep '^GITHUB_TOKEN=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
68
|
+
[ -n "$github_token" ] || exit 1
|
|
69
|
+
encoded_slug="$(printf '%s' "$slug" | jq -sRr @uri)"
|
|
70
|
+
recovered="$(curl -sf "$api_url/api/org/$encoded_slug/key" \
|
|
71
|
+
-H "Authorization: Bearer $github_token" --connect-timeout 2 --max-time 12 \
|
|
72
|
+
2>/dev/null || true)"
|
|
73
|
+
printf '%s' "$recovered" | jq -e --arg slug "$slug" '
|
|
74
|
+
(.api_key | type == "string" and length > 0) and .org_slug == $slug
|
|
75
|
+
' >/dev/null 2>&1 || exit 1
|
|
76
|
+
printf '%s' "$recovered" | \
|
|
77
|
+
EGREGORE_ROOT="$ROOT" \
|
|
78
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
79
|
+
python3 -m egregore_runtime.identity_cli install-connected-key >/dev/null
|
|
80
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
81
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
82
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
83
|
+
2>/dev/null || true)"
|
|
84
|
+
printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
85
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
86
|
+
' >/dev/null 2>&1 || exit 1
|
|
87
|
+
fi
|
|
52
88
|
roster="$(curl -sf "$api_url/api/org/$slug/members?include_removed=true" \
|
|
53
89
|
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
54
90
|
2>/dev/null || true)"
|
|
@@ -58,7 +94,9 @@ case "${1:-ensure}" in
|
|
|
58
94
|
--argjson organization "$control_plane" \
|
|
59
95
|
--argjson roster "$roster" \
|
|
60
96
|
'{organization:$organization,roster:$roster}' | \
|
|
61
|
-
EGREGORE_ROOT="$ROOT"
|
|
97
|
+
EGREGORE_ROOT="$ROOT" \
|
|
98
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
99
|
+
python3 -m egregore_runtime.identity_cli \
|
|
62
100
|
reconcile-records >/dev/null
|
|
63
101
|
fi
|
|
64
102
|
resolved
|
|
@@ -28,13 +28,35 @@ _runtime() {
|
|
|
28
28
|
python3 -m egregore_runtime.harness_cli "$@"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
_ensure_identity() {
|
|
32
|
+
# Pre-Runtime instances can predate stable organization identity. Resolve it
|
|
33
|
+
# through the existing verified Local/Connected migration before computing
|
|
34
|
+
# the instance-scoped upgrade key; never infer identity from path or slug.
|
|
35
|
+
if jq -e '
|
|
36
|
+
.org_id | type == "string" and length > 0 and (startswith("legacy-org:") | not)
|
|
37
|
+
' "$INSTANCE_ROOT/egregore.json" >/dev/null 2>&1; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
EGREGORE_INSTANCE_ROOT="$INSTANCE_ROOT" \
|
|
41
|
+
PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
|
|
42
|
+
bash "$SCRIPT_DIR/bin/runtime-identity.sh" ensure
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_run_upgrade() {
|
|
46
|
+
_ensure_identity || {
|
|
47
|
+
echo "Runtime update could not resolve this Egregore's stable identity." >&2
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
_runtime upgrade "$@"
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
case "${1:-}" in
|
|
32
|
-
status) shift;
|
|
33
|
-
init) shift;
|
|
34
|
-
prepare) shift;
|
|
35
|
-
activate) shift;
|
|
36
|
-
cancel) shift;
|
|
37
|
-
rollback) shift;
|
|
54
|
+
status) shift; _run_upgrade status "$@" ;;
|
|
55
|
+
init) shift; _run_upgrade init "$@" ;;
|
|
56
|
+
prepare) shift; _run_upgrade prepare "$@" ;;
|
|
57
|
+
activate) shift; _run_upgrade activate "$@" ;;
|
|
58
|
+
cancel) shift; _run_upgrade cancel "$@" ;;
|
|
59
|
+
rollback) shift; _run_upgrade rollback "$@" ;;
|
|
38
60
|
team-sync) shift; _runtime team-sync "$@" ;;
|
|
39
61
|
*)
|
|
40
62
|
echo "usage: bash bin/runtime-upgrade.sh {status|init --candidate <version>|prepare|activate|cancel|rollback|team-sync}" >&2
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from .contracts import Permission
|
|
@@ -44,6 +45,55 @@ def _authorize_self(root: Path, permission: Permission, resource: str) -> int:
|
|
|
44
45
|
return 0
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def _install_connected_key(root: Path, payload: dict) -> dict[str, str]:
|
|
49
|
+
"""Install a membership-verified key without exposing it in output."""
|
|
50
|
+
|
|
51
|
+
config = json.loads((root / "egregore.json").read_text(encoding="utf-8"))
|
|
52
|
+
expected_slug = str(config.get("slug") or "").strip()
|
|
53
|
+
returned_slug = str(payload.get("org_slug") or "").strip()
|
|
54
|
+
api_key = str(payload.get("api_key") or "").strip()
|
|
55
|
+
if not expected_slug or returned_slug != expected_slug:
|
|
56
|
+
raise ValueError("recovered credential does not match the configured organization")
|
|
57
|
+
if not api_key or "\n" in api_key or "\r" in api_key:
|
|
58
|
+
raise ValueError("recovered credential is missing or malformed")
|
|
59
|
+
|
|
60
|
+
env_path = root / ".env"
|
|
61
|
+
existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
62
|
+
output: list[str] = []
|
|
63
|
+
replaced = False
|
|
64
|
+
for line in existing.splitlines():
|
|
65
|
+
if line.startswith("EGREGORE_API_KEY="):
|
|
66
|
+
if not replaced:
|
|
67
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
68
|
+
replaced = True
|
|
69
|
+
continue
|
|
70
|
+
output.append(line)
|
|
71
|
+
if not replaced:
|
|
72
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
73
|
+
|
|
74
|
+
temporary: Path | None = None
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.NamedTemporaryFile(
|
|
77
|
+
mode="w",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
dir=root,
|
|
80
|
+
prefix=".env.egregore-key.",
|
|
81
|
+
delete=False,
|
|
82
|
+
) as handle:
|
|
83
|
+
temporary = Path(handle.name)
|
|
84
|
+
os.chmod(temporary, 0o600)
|
|
85
|
+
handle.write("\n".join(output) + "\n")
|
|
86
|
+
handle.flush()
|
|
87
|
+
os.fsync(handle.fileno())
|
|
88
|
+
os.replace(temporary, env_path)
|
|
89
|
+
temporary = None
|
|
90
|
+
finally:
|
|
91
|
+
if temporary is not None:
|
|
92
|
+
temporary.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
return {"org_slug": expected_slug}
|
|
95
|
+
|
|
96
|
+
|
|
47
97
|
def main() -> int:
|
|
48
98
|
root = Path(os.environ.get("EGREGORE_ROOT") or Path.cwd()).resolve()
|
|
49
99
|
if len(sys.argv) > 1 and sys.argv[1] == "initialize-local":
|
|
@@ -77,6 +127,11 @@ def main() -> int:
|
|
|
77
127
|
result = reconcile_control_plane_identity(root, response)
|
|
78
128
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
79
129
|
return 0
|
|
130
|
+
if len(sys.argv) > 1 and sys.argv[1] == "install-connected-key":
|
|
131
|
+
payload = json.load(sys.stdin)
|
|
132
|
+
result = _install_connected_key(root, payload)
|
|
133
|
+
print(json.dumps({"status": "installed", **result}, sort_keys=True))
|
|
134
|
+
return 0
|
|
80
135
|
payload = json.load(sys.stdin)
|
|
81
136
|
result = reconcile_control_plane_identity(root, payload)
|
|
82
137
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
@@ -6,7 +6,12 @@ set -euo pipefail
|
|
|
6
6
|
# legacy Local instances create opaque local IDs once. Network failure remains
|
|
7
7
|
# non-fatal to startup and is retried next session.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
RUNTIME_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
# A launcher-carried bridge runs outside the selected instance. Keep code
|
|
11
|
+
# rooted in the packaged Runtime while resolving and persisting identity in
|
|
12
|
+
# exactly the instance the launcher selected.
|
|
13
|
+
ROOT="${EGREGORE_INSTANCE_ROOT:-$RUNTIME_ROOT}"
|
|
14
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
10
15
|
CONFIG="$ROOT/egregore.json"
|
|
11
16
|
STATE="$ROOT/.egregore-state.json"
|
|
12
17
|
|
|
@@ -34,21 +39,52 @@ case "${1:-ensure}" in
|
|
|
34
39
|
resolved && exit 0
|
|
35
40
|
mode="$(jq -r 'if (.mode // "local") == "connected" or (.api_url // "") != "" then "connected" else "local" end' "$CONFIG" 2>/dev/null || echo local)"
|
|
36
41
|
if [ "$mode" = "local" ]; then
|
|
37
|
-
EGREGORE_ROOT="$ROOT"
|
|
42
|
+
EGREGORE_ROOT="$ROOT" \
|
|
43
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
44
|
+
python3 -m egregore_runtime.identity_cli initialize-local >/dev/null
|
|
38
45
|
else
|
|
39
46
|
api_url="$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)"
|
|
40
|
-
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
41
|
-
[ -n "$api_url" ] && [ -n "$api_key" ] || exit 1
|
|
42
|
-
# Identity repair is read-only: select the already-existing account and
|
|
43
|
-
# membership records, then adopt their opaque IDs locally. If migration
|
|
44
|
-
# 038 has not reached the control plane, the next session retries.
|
|
45
|
-
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
46
|
-
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
47
|
-
2>/dev/null || true)"
|
|
48
|
-
printf '%s' "$control_plane" | jq -e \
|
|
49
|
-
'.org_id | type == "string" and length > 0' >/dev/null 2>&1 || exit 1
|
|
50
47
|
slug="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
|
|
51
|
-
|
|
48
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
49
|
+
[ -n "$api_url" ] && [ -n "$slug" ] || exit 1
|
|
50
|
+
# Identity repair selects already-existing account and membership
|
|
51
|
+
# records, then adopts their opaque IDs locally. If a stale credential
|
|
52
|
+
# belongs to another org, recover only the configured org's existing key
|
|
53
|
+
# through verified GitHub membership. No control-plane records are made.
|
|
54
|
+
control_plane=""
|
|
55
|
+
if [ -n "$api_key" ]; then
|
|
56
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
57
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
58
|
+
2>/dev/null || true)"
|
|
59
|
+
fi
|
|
60
|
+
if ! printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
61
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
62
|
+
' >/dev/null 2>&1; then
|
|
63
|
+
# A historic path/namespace collision could leave this checkout with
|
|
64
|
+
# another Egregore's API key. Recover the configured org's existing key
|
|
65
|
+
# only through the membership-verifying endpoint, install it atomically,
|
|
66
|
+
# and retry. No credential bytes are printed.
|
|
67
|
+
github_token="$(grep '^GITHUB_TOKEN=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
68
|
+
[ -n "$github_token" ] || exit 1
|
|
69
|
+
encoded_slug="$(printf '%s' "$slug" | jq -sRr @uri)"
|
|
70
|
+
recovered="$(curl -sf "$api_url/api/org/$encoded_slug/key" \
|
|
71
|
+
-H "Authorization: Bearer $github_token" --connect-timeout 2 --max-time 12 \
|
|
72
|
+
2>/dev/null || true)"
|
|
73
|
+
printf '%s' "$recovered" | jq -e --arg slug "$slug" '
|
|
74
|
+
(.api_key | type == "string" and length > 0) and .org_slug == $slug
|
|
75
|
+
' >/dev/null 2>&1 || exit 1
|
|
76
|
+
printf '%s' "$recovered" | \
|
|
77
|
+
EGREGORE_ROOT="$ROOT" \
|
|
78
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
79
|
+
python3 -m egregore_runtime.identity_cli install-connected-key >/dev/null
|
|
80
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
81
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
82
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
83
|
+
2>/dev/null || true)"
|
|
84
|
+
printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
85
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
86
|
+
' >/dev/null 2>&1 || exit 1
|
|
87
|
+
fi
|
|
52
88
|
roster="$(curl -sf "$api_url/api/org/$slug/members?include_removed=true" \
|
|
53
89
|
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
54
90
|
2>/dev/null || true)"
|
|
@@ -58,7 +94,9 @@ case "${1:-ensure}" in
|
|
|
58
94
|
--argjson organization "$control_plane" \
|
|
59
95
|
--argjson roster "$roster" \
|
|
60
96
|
'{organization:$organization,roster:$roster}' | \
|
|
61
|
-
EGREGORE_ROOT="$ROOT"
|
|
97
|
+
EGREGORE_ROOT="$ROOT" \
|
|
98
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
99
|
+
python3 -m egregore_runtime.identity_cli \
|
|
62
100
|
reconcile-records >/dev/null
|
|
63
101
|
fi
|
|
64
102
|
resolved
|
|
@@ -28,13 +28,35 @@ _runtime() {
|
|
|
28
28
|
python3 -m egregore_runtime.harness_cli "$@"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
_ensure_identity() {
|
|
32
|
+
# Pre-Runtime instances can predate stable organization identity. Resolve it
|
|
33
|
+
# through the existing verified Local/Connected migration before computing
|
|
34
|
+
# the instance-scoped upgrade key; never infer identity from path or slug.
|
|
35
|
+
if jq -e '
|
|
36
|
+
.org_id | type == "string" and length > 0 and (startswith("legacy-org:") | not)
|
|
37
|
+
' "$INSTANCE_ROOT/egregore.json" >/dev/null 2>&1; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
EGREGORE_INSTANCE_ROOT="$INSTANCE_ROOT" \
|
|
41
|
+
PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
|
|
42
|
+
bash "$SCRIPT_DIR/bin/runtime-identity.sh" ensure
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_run_upgrade() {
|
|
46
|
+
_ensure_identity || {
|
|
47
|
+
echo "Runtime update could not resolve this Egregore's stable identity." >&2
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
_runtime upgrade "$@"
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
case "${1:-}" in
|
|
32
|
-
status) shift;
|
|
33
|
-
init) shift;
|
|
34
|
-
prepare) shift;
|
|
35
|
-
activate) shift;
|
|
36
|
-
cancel) shift;
|
|
37
|
-
rollback) shift;
|
|
54
|
+
status) shift; _run_upgrade status "$@" ;;
|
|
55
|
+
init) shift; _run_upgrade init "$@" ;;
|
|
56
|
+
prepare) shift; _run_upgrade prepare "$@" ;;
|
|
57
|
+
activate) shift; _run_upgrade activate "$@" ;;
|
|
58
|
+
cancel) shift; _run_upgrade cancel "$@" ;;
|
|
59
|
+
rollback) shift; _run_upgrade rollback "$@" ;;
|
|
38
60
|
team-sync) shift; _runtime team-sync "$@" ;;
|
|
39
61
|
*)
|
|
40
62
|
echo "usage: bash bin/runtime-upgrade.sh {status|init --candidate <version>|prepare|activate|cancel|rollback|team-sync}" >&2
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from .contracts import Permission
|
|
@@ -44,6 +45,55 @@ def _authorize_self(root: Path, permission: Permission, resource: str) -> int:
|
|
|
44
45
|
return 0
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def _install_connected_key(root: Path, payload: dict) -> dict[str, str]:
|
|
49
|
+
"""Install a membership-verified key without exposing it in output."""
|
|
50
|
+
|
|
51
|
+
config = json.loads((root / "egregore.json").read_text(encoding="utf-8"))
|
|
52
|
+
expected_slug = str(config.get("slug") or "").strip()
|
|
53
|
+
returned_slug = str(payload.get("org_slug") or "").strip()
|
|
54
|
+
api_key = str(payload.get("api_key") or "").strip()
|
|
55
|
+
if not expected_slug or returned_slug != expected_slug:
|
|
56
|
+
raise ValueError("recovered credential does not match the configured organization")
|
|
57
|
+
if not api_key or "\n" in api_key or "\r" in api_key:
|
|
58
|
+
raise ValueError("recovered credential is missing or malformed")
|
|
59
|
+
|
|
60
|
+
env_path = root / ".env"
|
|
61
|
+
existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
62
|
+
output: list[str] = []
|
|
63
|
+
replaced = False
|
|
64
|
+
for line in existing.splitlines():
|
|
65
|
+
if line.startswith("EGREGORE_API_KEY="):
|
|
66
|
+
if not replaced:
|
|
67
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
68
|
+
replaced = True
|
|
69
|
+
continue
|
|
70
|
+
output.append(line)
|
|
71
|
+
if not replaced:
|
|
72
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
73
|
+
|
|
74
|
+
temporary: Path | None = None
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.NamedTemporaryFile(
|
|
77
|
+
mode="w",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
dir=root,
|
|
80
|
+
prefix=".env.egregore-key.",
|
|
81
|
+
delete=False,
|
|
82
|
+
) as handle:
|
|
83
|
+
temporary = Path(handle.name)
|
|
84
|
+
os.chmod(temporary, 0o600)
|
|
85
|
+
handle.write("\n".join(output) + "\n")
|
|
86
|
+
handle.flush()
|
|
87
|
+
os.fsync(handle.fileno())
|
|
88
|
+
os.replace(temporary, env_path)
|
|
89
|
+
temporary = None
|
|
90
|
+
finally:
|
|
91
|
+
if temporary is not None:
|
|
92
|
+
temporary.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
return {"org_slug": expected_slug}
|
|
95
|
+
|
|
96
|
+
|
|
47
97
|
def main() -> int:
|
|
48
98
|
root = Path(os.environ.get("EGREGORE_ROOT") or Path.cwd()).resolve()
|
|
49
99
|
if len(sys.argv) > 1 and sys.argv[1] == "initialize-local":
|
|
@@ -77,6 +127,11 @@ def main() -> int:
|
|
|
77
127
|
result = reconcile_control_plane_identity(root, response)
|
|
78
128
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
79
129
|
return 0
|
|
130
|
+
if len(sys.argv) > 1 and sys.argv[1] == "install-connected-key":
|
|
131
|
+
payload = json.load(sys.stdin)
|
|
132
|
+
result = _install_connected_key(root, payload)
|
|
133
|
+
print(json.dumps({"status": "installed", **result}, sort_keys=True))
|
|
134
|
+
return 0
|
|
80
135
|
payload = json.load(sys.stdin)
|
|
81
136
|
result = reconcile_control_plane_identity(root, payload)
|
|
82
137
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
@@ -6,7 +6,12 @@ set -euo pipefail
|
|
|
6
6
|
# legacy Local instances create opaque local IDs once. Network failure remains
|
|
7
7
|
# non-fatal to startup and is retried next session.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
RUNTIME_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
# A launcher-carried bridge runs outside the selected instance. Keep code
|
|
11
|
+
# rooted in the packaged Runtime while resolving and persisting identity in
|
|
12
|
+
# exactly the instance the launcher selected.
|
|
13
|
+
ROOT="${EGREGORE_INSTANCE_ROOT:-$RUNTIME_ROOT}"
|
|
14
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
10
15
|
CONFIG="$ROOT/egregore.json"
|
|
11
16
|
STATE="$ROOT/.egregore-state.json"
|
|
12
17
|
|
|
@@ -34,21 +39,52 @@ case "${1:-ensure}" in
|
|
|
34
39
|
resolved && exit 0
|
|
35
40
|
mode="$(jq -r 'if (.mode // "local") == "connected" or (.api_url // "") != "" then "connected" else "local" end' "$CONFIG" 2>/dev/null || echo local)"
|
|
36
41
|
if [ "$mode" = "local" ]; then
|
|
37
|
-
EGREGORE_ROOT="$ROOT"
|
|
42
|
+
EGREGORE_ROOT="$ROOT" \
|
|
43
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
44
|
+
python3 -m egregore_runtime.identity_cli initialize-local >/dev/null
|
|
38
45
|
else
|
|
39
46
|
api_url="$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)"
|
|
40
|
-
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
41
|
-
[ -n "$api_url" ] && [ -n "$api_key" ] || exit 1
|
|
42
|
-
# Identity repair is read-only: select the already-existing account and
|
|
43
|
-
# membership records, then adopt their opaque IDs locally. If migration
|
|
44
|
-
# 038 has not reached the control plane, the next session retries.
|
|
45
|
-
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
46
|
-
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
47
|
-
2>/dev/null || true)"
|
|
48
|
-
printf '%s' "$control_plane" | jq -e \
|
|
49
|
-
'.org_id | type == "string" and length > 0' >/dev/null 2>&1 || exit 1
|
|
50
47
|
slug="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
|
|
51
|
-
|
|
48
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
49
|
+
[ -n "$api_url" ] && [ -n "$slug" ] || exit 1
|
|
50
|
+
# Identity repair selects already-existing account and membership
|
|
51
|
+
# records, then adopts their opaque IDs locally. If a stale credential
|
|
52
|
+
# belongs to another org, recover only the configured org's existing key
|
|
53
|
+
# through verified GitHub membership. No control-plane records are made.
|
|
54
|
+
control_plane=""
|
|
55
|
+
if [ -n "$api_key" ]; then
|
|
56
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
57
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
58
|
+
2>/dev/null || true)"
|
|
59
|
+
fi
|
|
60
|
+
if ! printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
61
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
62
|
+
' >/dev/null 2>&1; then
|
|
63
|
+
# A historic path/namespace collision could leave this checkout with
|
|
64
|
+
# another Egregore's API key. Recover the configured org's existing key
|
|
65
|
+
# only through the membership-verifying endpoint, install it atomically,
|
|
66
|
+
# and retry. No credential bytes are printed.
|
|
67
|
+
github_token="$(grep '^GITHUB_TOKEN=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
68
|
+
[ -n "$github_token" ] || exit 1
|
|
69
|
+
encoded_slug="$(printf '%s' "$slug" | jq -sRr @uri)"
|
|
70
|
+
recovered="$(curl -sf "$api_url/api/org/$encoded_slug/key" \
|
|
71
|
+
-H "Authorization: Bearer $github_token" --connect-timeout 2 --max-time 12 \
|
|
72
|
+
2>/dev/null || true)"
|
|
73
|
+
printf '%s' "$recovered" | jq -e --arg slug "$slug" '
|
|
74
|
+
(.api_key | type == "string" and length > 0) and .org_slug == $slug
|
|
75
|
+
' >/dev/null 2>&1 || exit 1
|
|
76
|
+
printf '%s' "$recovered" | \
|
|
77
|
+
EGREGORE_ROOT="$ROOT" \
|
|
78
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
79
|
+
python3 -m egregore_runtime.identity_cli install-connected-key >/dev/null
|
|
80
|
+
api_key="$(grep '^EGREGORE_API_KEY=' "$ROOT/.env" 2>/dev/null | cut -d= -f2-)"
|
|
81
|
+
control_plane="$(curl -sf "$api_url/api/org/status" \
|
|
82
|
+
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
83
|
+
2>/dev/null || true)"
|
|
84
|
+
printf '%s' "$control_plane" | jq -e --arg slug "$slug" '
|
|
85
|
+
(.org_id | type == "string" and length > 0) and .slug == $slug
|
|
86
|
+
' >/dev/null 2>&1 || exit 1
|
|
87
|
+
fi
|
|
52
88
|
roster="$(curl -sf "$api_url/api/org/$slug/members?include_removed=true" \
|
|
53
89
|
-H "Authorization: Bearer $api_key" --connect-timeout 2 --max-time 4 \
|
|
54
90
|
2>/dev/null || true)"
|
|
@@ -58,7 +94,9 @@ case "${1:-ensure}" in
|
|
|
58
94
|
--argjson organization "$control_plane" \
|
|
59
95
|
--argjson roster "$roster" \
|
|
60
96
|
'{organization:$organization,roster:$roster}' | \
|
|
61
|
-
EGREGORE_ROOT="$ROOT"
|
|
97
|
+
EGREGORE_ROOT="$ROOT" \
|
|
98
|
+
PYTHONPATH="$RUNTIME_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
99
|
+
python3 -m egregore_runtime.identity_cli \
|
|
62
100
|
reconcile-records >/dev/null
|
|
63
101
|
fi
|
|
64
102
|
resolved
|
|
@@ -28,13 +28,35 @@ _runtime() {
|
|
|
28
28
|
python3 -m egregore_runtime.harness_cli "$@"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
_ensure_identity() {
|
|
32
|
+
# Pre-Runtime instances can predate stable organization identity. Resolve it
|
|
33
|
+
# through the existing verified Local/Connected migration before computing
|
|
34
|
+
# the instance-scoped upgrade key; never infer identity from path or slug.
|
|
35
|
+
if jq -e '
|
|
36
|
+
.org_id | type == "string" and length > 0 and (startswith("legacy-org:") | not)
|
|
37
|
+
' "$INSTANCE_ROOT/egregore.json" >/dev/null 2>&1; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
EGREGORE_INSTANCE_ROOT="$INSTANCE_ROOT" \
|
|
41
|
+
PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
|
|
42
|
+
bash "$SCRIPT_DIR/bin/runtime-identity.sh" ensure
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_run_upgrade() {
|
|
46
|
+
_ensure_identity || {
|
|
47
|
+
echo "Runtime update could not resolve this Egregore's stable identity." >&2
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
_runtime upgrade "$@"
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
case "${1:-}" in
|
|
32
|
-
status) shift;
|
|
33
|
-
init) shift;
|
|
34
|
-
prepare) shift;
|
|
35
|
-
activate) shift;
|
|
36
|
-
cancel) shift;
|
|
37
|
-
rollback) shift;
|
|
54
|
+
status) shift; _run_upgrade status "$@" ;;
|
|
55
|
+
init) shift; _run_upgrade init "$@" ;;
|
|
56
|
+
prepare) shift; _run_upgrade prepare "$@" ;;
|
|
57
|
+
activate) shift; _run_upgrade activate "$@" ;;
|
|
58
|
+
cancel) shift; _run_upgrade cancel "$@" ;;
|
|
59
|
+
rollback) shift; _run_upgrade rollback "$@" ;;
|
|
38
60
|
team-sync) shift; _runtime team-sync "$@" ;;
|
|
39
61
|
*)
|
|
40
62
|
echo "usage: bash bin/runtime-upgrade.sh {status|init --candidate <version>|prepare|activate|cancel|rollback|team-sync}" >&2
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from .contracts import Permission
|
|
@@ -44,6 +45,55 @@ def _authorize_self(root: Path, permission: Permission, resource: str) -> int:
|
|
|
44
45
|
return 0
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def _install_connected_key(root: Path, payload: dict) -> dict[str, str]:
|
|
49
|
+
"""Install a membership-verified key without exposing it in output."""
|
|
50
|
+
|
|
51
|
+
config = json.loads((root / "egregore.json").read_text(encoding="utf-8"))
|
|
52
|
+
expected_slug = str(config.get("slug") or "").strip()
|
|
53
|
+
returned_slug = str(payload.get("org_slug") or "").strip()
|
|
54
|
+
api_key = str(payload.get("api_key") or "").strip()
|
|
55
|
+
if not expected_slug or returned_slug != expected_slug:
|
|
56
|
+
raise ValueError("recovered credential does not match the configured organization")
|
|
57
|
+
if not api_key or "\n" in api_key or "\r" in api_key:
|
|
58
|
+
raise ValueError("recovered credential is missing or malformed")
|
|
59
|
+
|
|
60
|
+
env_path = root / ".env"
|
|
61
|
+
existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
62
|
+
output: list[str] = []
|
|
63
|
+
replaced = False
|
|
64
|
+
for line in existing.splitlines():
|
|
65
|
+
if line.startswith("EGREGORE_API_KEY="):
|
|
66
|
+
if not replaced:
|
|
67
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
68
|
+
replaced = True
|
|
69
|
+
continue
|
|
70
|
+
output.append(line)
|
|
71
|
+
if not replaced:
|
|
72
|
+
output.append(f"EGREGORE_API_KEY={api_key}")
|
|
73
|
+
|
|
74
|
+
temporary: Path | None = None
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.NamedTemporaryFile(
|
|
77
|
+
mode="w",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
dir=root,
|
|
80
|
+
prefix=".env.egregore-key.",
|
|
81
|
+
delete=False,
|
|
82
|
+
) as handle:
|
|
83
|
+
temporary = Path(handle.name)
|
|
84
|
+
os.chmod(temporary, 0o600)
|
|
85
|
+
handle.write("\n".join(output) + "\n")
|
|
86
|
+
handle.flush()
|
|
87
|
+
os.fsync(handle.fileno())
|
|
88
|
+
os.replace(temporary, env_path)
|
|
89
|
+
temporary = None
|
|
90
|
+
finally:
|
|
91
|
+
if temporary is not None:
|
|
92
|
+
temporary.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
return {"org_slug": expected_slug}
|
|
95
|
+
|
|
96
|
+
|
|
47
97
|
def main() -> int:
|
|
48
98
|
root = Path(os.environ.get("EGREGORE_ROOT") or Path.cwd()).resolve()
|
|
49
99
|
if len(sys.argv) > 1 and sys.argv[1] == "initialize-local":
|
|
@@ -77,6 +127,11 @@ def main() -> int:
|
|
|
77
127
|
result = reconcile_control_plane_identity(root, response)
|
|
78
128
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|
|
79
129
|
return 0
|
|
130
|
+
if len(sys.argv) > 1 and sys.argv[1] == "install-connected-key":
|
|
131
|
+
payload = json.load(sys.stdin)
|
|
132
|
+
result = _install_connected_key(root, payload)
|
|
133
|
+
print(json.dumps({"status": "installed", **result}, sort_keys=True))
|
|
134
|
+
return 0
|
|
80
135
|
payload = json.load(sys.stdin)
|
|
81
136
|
result = reconcile_control_plane_identity(root, payload)
|
|
82
137
|
print(json.dumps({"status": "reconciled", **result}, sort_keys=True))
|