create-egregore 0.21.0-runtime-mvp.8 → 0.21.0-runtime-mvp.10
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 +126 -14
- package/package.json +1 -1
- package/runtime/claude/bin/search.sh +15 -0
- package/runtime/claude/bin/settings.sh +37 -3
- package/runtime/claude/bin/telemetry.sh +60 -24
- package/runtime/codex/bin/search.sh +15 -0
- package/runtime/codex/bin/settings.sh +37 -3
- package/runtime/codex/bin/telemetry.sh +60 -24
- package/runtime/pi/bin/search.sh +15 -0
- package/runtime/pi/bin/settings.sh +37 -3
- package/runtime/pi/bin/telemetry.sh +60 -24
- package/runtime/prime/bin/search.sh +15 -0
- package/runtime/prime/bin/settings.sh +37 -3
- package/runtime/prime/bin/telemetry.sh +60 -24
|
@@ -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.10
|
|
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.10";
|
|
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");
|
|
@@ -2787,13 +2787,13 @@ async function cmdSettingsRuntime(inst, args) {
|
|
|
2787
2787
|
async function cmdSettings(args) {
|
|
2788
2788
|
const registry = readRegistry();
|
|
2789
2789
|
if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
|
|
2790
|
-
const DOMAINS = new Set(["hosting", "workflow", "repo", "admin", "people", "dump", "runtime", "privacy", "telemetry", "boundary"]);
|
|
2790
|
+
const DOMAINS = new Set(["hosting", "workflow", "repo", "admin", "people", "dump", "runtime", "privacy", "telemetry", "boundary", "posture"]);
|
|
2791
2791
|
let rest = args.slice();
|
|
2792
2792
|
let slug = "";
|
|
2793
2793
|
if (rest.length && !DOMAINS.has(rest[0])) { slug = rest[0]; rest = rest.slice(1); }
|
|
2794
2794
|
const inst = resolveInviteInstance(registry, slug);
|
|
2795
2795
|
if (!inst) {
|
|
2796
|
-
die(`multiple egregores installed; choose one:\n egregore settings <slug> [runtime|hosting|workflow|repo|admin|people|privacy|telemetry]\n run: egregore ls`);
|
|
2796
|
+
die(`multiple egregores installed; choose one:\n egregore settings <slug> [runtime|hosting|workflow|repo|admin|people|privacy|posture|telemetry]\n run: egregore ls`);
|
|
2797
2797
|
}
|
|
2798
2798
|
// Runtime update is Runtime-owned state, not org config — it routes to the
|
|
2799
2799
|
// upgrade bridge instead of bin/settings.sh.
|
|
@@ -3208,14 +3208,58 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
|
|
|
3208
3208
|
lines.push(boxLine(`${BOLD}${view === "privacy" ? "Privacy & access" : "Telemetry"}${RESET}`, innerWidth, margin));
|
|
3209
3209
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3210
3210
|
if (mode === "shareinfo") {
|
|
3211
|
-
const
|
|
3211
|
+
const proposal = data && data.shareProposal;
|
|
3212
3212
|
lines.push(boxLine(`${BOLD}Share proposal${RESET} ${DIM}(nothing has been sent)${RESET}`, innerWidth, margin));
|
|
3213
3213
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3214
|
-
|
|
3215
|
-
lines.push(boxLine(`${
|
|
3214
|
+
if (proposal && proposal.token) {
|
|
3215
|
+
lines.push(boxLine(`${DIM}Target: ${proposal.target}${RESET}`, innerWidth, margin));
|
|
3216
|
+
lines.push(boxLine(`${DIM}Events: ${proposal.events} · Bytes: ${proposal.bytes}${RESET}`, innerWidth, margin));
|
|
3217
|
+
lines.push(boxLine(`${DIM}Dataset SHA-256: ${proposal.dataset_sha256}${RESET}`, innerWidth, margin));
|
|
3218
|
+
lines.push(boxLine(`${DIM}Metric fields: ${proposal.metric_fields}${RESET}`, innerWidth, margin));
|
|
3219
|
+
lines.push(boxLine(`${DIM}Excluded by validation: ${proposal.excluded}${RESET}`, innerWidth, margin));
|
|
3220
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3221
|
+
if (input) {
|
|
3222
|
+
lines.push(boxLine(`${YELLOW}${input}${RESET}`, innerWidth, margin));
|
|
3223
|
+
} else {
|
|
3224
|
+
lines.push(boxLine(`${DIM}Nothing is sent until you confirm here. ${RESET}${BOLD}s${RESET}${DIM} send now · esc back${RESET}`, innerWidth, margin));
|
|
3225
|
+
}
|
|
3226
|
+
} else {
|
|
3227
|
+
// Older adapter without a machine-readable proposal: show its prose
|
|
3228
|
+
// and fall back to the terminal confirm instruction it printed.
|
|
3229
|
+
const shareLines = (data && data.shareLines) || ["share proposal unavailable"];
|
|
3230
|
+
for (const line of shareLines.slice(0, 16)) {
|
|
3231
|
+
lines.push(boxLine(`${line.includes("--confirm") ? "" : DIM}${line}${RESET}`, innerWidth, margin));
|
|
3232
|
+
}
|
|
3233
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3234
|
+
lines.push(boxLine(`${DIM}Sharing completes only by running the confirm command above in a terminal. ← back${RESET}`, innerWidth, margin));
|
|
3216
3235
|
}
|
|
3217
3236
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3218
|
-
lines.push(
|
|
3237
|
+
lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
|
|
3238
|
+
return lines;
|
|
3239
|
+
}
|
|
3240
|
+
if (mode === "posture") {
|
|
3241
|
+
const current = (data && data.privacySnapshot && data.privacySnapshot.boundary && data.privacySnapshot.boundary.posture) || "standard";
|
|
3242
|
+
lines.push(boxLine(`${BOLD}Boundary posture${RESET} ${DIM}— how far reads may reach outside this egregore${RESET}`, innerWidth, margin));
|
|
3243
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3244
|
+
const options = [
|
|
3245
|
+
["strict", "reads outside the project, memory, and managed repos always ask first"],
|
|
3246
|
+
["standard", "inbox folders (Downloads, Desktop) readable without asking; everything else asks first"],
|
|
3247
|
+
["open", "reads outside the boundary allowed without asking; writes outside still ask"],
|
|
3248
|
+
];
|
|
3249
|
+
options.forEach(([name, why], i) => {
|
|
3250
|
+
const sel = i === (listCursor || 0);
|
|
3251
|
+
const mark = name === current ? `${GREEN}● ${RESET}` : " ";
|
|
3252
|
+
lines.push(boxLine(`${sel ? `${CYAN}❯${RESET} ` : " "}${mark}${sel ? BOLD : ""}${name}${RESET}`, innerWidth, margin));
|
|
3253
|
+
lines.push(boxLine(` ${DIM}${why}${RESET}`, innerWidth, margin));
|
|
3254
|
+
});
|
|
3255
|
+
lines.push(boxLine(` ${DIM}other Egregore instances stay isolated under every posture${RESET}`, innerWidth, margin));
|
|
3256
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3257
|
+
if (input) {
|
|
3258
|
+
lines.push(boxLine(`${YELLOW}${input}${RESET}`, innerWidth, margin));
|
|
3259
|
+
} else {
|
|
3260
|
+
if (notice) lines.push(boxLine(notice, innerWidth, margin));
|
|
3261
|
+
lines.push(boxLine(`${DIM}↑↓ nav · ${RESET}${BOLD}enter${RESET}${DIM} select · esc cancel${RESET}`, innerWidth, margin));
|
|
3262
|
+
}
|
|
3219
3263
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3220
3264
|
lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
|
|
3221
3265
|
return lines;
|
|
@@ -3300,7 +3344,7 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
|
|
|
3300
3344
|
lines.push(boxLine(
|
|
3301
3345
|
locked
|
|
3302
3346
|
? `${DIM}Boundary policy is managed by your organization. ← back · q quit${RESET}`
|
|
3303
|
-
: `${DIM}${RESET}${BOLD}a${RESET}${DIM} add personal read root · ${RESET}${BOLD}r${RESET}${DIM} remove · ← back · q quit${RESET}`,
|
|
3347
|
+
: `${DIM}${RESET}${BOLD}a${RESET}${DIM} add personal read root · ${RESET}${BOLD}r${RESET}${DIM} remove · ${RESET}${BOLD}p${RESET}${DIM} boundary posture · ← back · q quit${RESET}`,
|
|
3304
3348
|
innerWidth, margin,
|
|
3305
3349
|
));
|
|
3306
3350
|
} else {
|
|
@@ -3966,16 +4010,49 @@ async function browser(args = {}) {
|
|
|
3966
4010
|
render(); return;
|
|
3967
4011
|
}
|
|
3968
4012
|
if (chunk === "s") {
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4013
|
+
// Prefer the machine-readable proposal so the send can complete
|
|
4014
|
+
// here; fall back to the adapter's prose for older runtimes.
|
|
4015
|
+
let proposal = null;
|
|
4016
|
+
const rj = runTelemetry(S.settingsInst, ["share", "--json"]);
|
|
4017
|
+
if (rj && rj.status === 0 && rj.stdout) {
|
|
4018
|
+
try { proposal = JSON.parse(rj.stdout); } catch { proposal = null; }
|
|
4019
|
+
}
|
|
4020
|
+
if (proposal && proposal.token) {
|
|
4021
|
+
S.settingsData = { ...(S.settingsData || {}), shareProposal: proposal, shareLines: null };
|
|
4022
|
+
} else {
|
|
4023
|
+
const r = runTelemetry(S.settingsInst, ["share"]);
|
|
4024
|
+
const shareLines = ((r && (r.stdout || r.stderr)) || "share preview unavailable").split("\n").filter(Boolean);
|
|
4025
|
+
S.settingsData = { ...(S.settingsData || {}), shareProposal: null, shareLines };
|
|
4026
|
+
}
|
|
4027
|
+
S.settingsMode = "shareinfo"; S.settingsNotice = ""; S.settingsInput = ""; S.settingsAction = "";
|
|
3973
4028
|
render(); return;
|
|
3974
4029
|
}
|
|
3975
4030
|
return;
|
|
3976
4031
|
}
|
|
3977
4032
|
if (S.settingsMode === "shareinfo") {
|
|
3978
|
-
|
|
4033
|
+
const proposal = S.settingsData && S.settingsData.shareProposal;
|
|
4034
|
+
if (S.settingsAction === "tele-share" && S.settingsInput) {
|
|
4035
|
+
if (chunk === "y" || chunk === "Y") {
|
|
4036
|
+
const r = runTelemetry(S.settingsInst, ["share", "--confirm", proposal.token]);
|
|
4037
|
+
S.settingsNotice = r && r.status === 0
|
|
4038
|
+
? `${GREEN}${((r.stdout || "shared").split("\n")[0])}${RESET}`
|
|
4039
|
+
: `${YELLOW}${(((r && (r.stderr || r.stdout)) || "share failed").split("\n")[0])}${RESET}`;
|
|
4040
|
+
S.settingsAction = ""; S.settingsInput = "";
|
|
4041
|
+
S.settingsData = { ...(S.settingsData || {}), shareProposal: null, telemetryEvents: telemetryEvents(S.settingsInst, 100) };
|
|
4042
|
+
S.settingsMode = "events"; S.settingsListCursor = 0;
|
|
4043
|
+
refreshPanel(); render(); return;
|
|
4044
|
+
}
|
|
4045
|
+
S.settingsAction = ""; S.settingsInput = ""; render(); return;
|
|
4046
|
+
}
|
|
4047
|
+
if (chunk === "s" && proposal && proposal.token) {
|
|
4048
|
+
S.settingsAction = "tele-share";
|
|
4049
|
+
S.settingsInput = `Send ${proposal.events} event(s) now? Local events are retained. [y/N]`;
|
|
4050
|
+
render(); return;
|
|
4051
|
+
}
|
|
4052
|
+
if (key.name === "escape" || key.name === "left") {
|
|
4053
|
+
S.settingsAction = ""; S.settingsInput = "";
|
|
4054
|
+
S.settingsMode = "events"; render(); return;
|
|
4055
|
+
}
|
|
3979
4056
|
return;
|
|
3980
4057
|
}
|
|
3981
4058
|
if (S.settingsMode === "pickdir") {
|
|
@@ -4003,6 +4080,34 @@ async function browser(args = {}) {
|
|
|
4003
4080
|
}
|
|
4004
4081
|
return;
|
|
4005
4082
|
}
|
|
4083
|
+
if (S.settingsMode === "posture") {
|
|
4084
|
+
const POSTURES = ["strict", "standard", "open"];
|
|
4085
|
+
const current = (S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.posture) || "standard";
|
|
4086
|
+
if (S.settingsAction === "posture" && S.settingsInput) {
|
|
4087
|
+
if (chunk === "y" || chunk === "Y") {
|
|
4088
|
+
const r = runSettings(S.settingsInst, ["posture", S.settingsPendingDir]);
|
|
4089
|
+
S.settingsNotice = r && r.status === 0
|
|
4090
|
+
? `${GREEN}${((r.stdout || "done").split("\n")[0])}${RESET}`
|
|
4091
|
+
: `${YELLOW}${(((r && (r.stderr || r.stdout)) || "posture change failed").split("\n")[0])}${RESET}`;
|
|
4092
|
+
S.settingsAction = ""; S.settingsInput = ""; S.settingsPendingDir = "";
|
|
4093
|
+
S.settingsMode = "browse";
|
|
4094
|
+
refreshPanel(); render(); return;
|
|
4095
|
+
}
|
|
4096
|
+
S.settingsAction = ""; S.settingsInput = ""; S.settingsPendingDir = ""; render(); return;
|
|
4097
|
+
}
|
|
4098
|
+
if (key.name === "escape" || key.name === "left") { S.settingsMode = "browse"; S.settingsListCursor = 0; S.settingsNotice = ""; render(); return; }
|
|
4099
|
+
if (key.name === "up" || key.name === "k") { S.settingsListCursor = ((S.settingsListCursor || 0) + 2) % 3; render(); return; }
|
|
4100
|
+
if (key.name === "down" || key.name === "j") { S.settingsListCursor = ((S.settingsListCursor || 0) + 1) % 3; render(); return; }
|
|
4101
|
+
if (key.name === "return") {
|
|
4102
|
+
const chosen = POSTURES[Math.min(S.settingsListCursor || 0, 2)];
|
|
4103
|
+
if (chosen === current) { S.settingsNotice = `${DIM}already on ${chosen}${RESET}`; render(); return; }
|
|
4104
|
+
S.settingsAction = "posture";
|
|
4105
|
+
S.settingsPendingDir = chosen;
|
|
4106
|
+
S.settingsInput = `Switch boundary posture to ${chosen.toUpperCase()} for the whole organization? This edits the org's committed egregore.json for everyone. [y/N]`;
|
|
4107
|
+
render(); return;
|
|
4108
|
+
}
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4006
4111
|
if (S.settingsMode === "pickroot") {
|
|
4007
4112
|
const roots = (S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.personal_read_roots) || [];
|
|
4008
4113
|
if (key.name === "escape" || key.name === "left") { S.settingsMode = "browse"; S.settingsListCursor = 0; render(); return; }
|
|
@@ -4050,6 +4155,13 @@ async function browser(args = {}) {
|
|
|
4050
4155
|
if (!locked && chunk === "r") {
|
|
4051
4156
|
S.settingsMode = "pickroot"; S.settingsListCursor = 0; S.settingsNotice = ""; render(); return;
|
|
4052
4157
|
}
|
|
4158
|
+
if (!locked && chunk === "p") {
|
|
4159
|
+
const current = (S.settingsData && S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.posture) || "standard";
|
|
4160
|
+
S.settingsMode = "posture";
|
|
4161
|
+
S.settingsListCursor = Math.max(0, ["strict", "standard", "open"].indexOf(current));
|
|
4162
|
+
S.settingsNotice = ""; S.settingsInput = ""; S.settingsAction = "";
|
|
4163
|
+
render(); return;
|
|
4164
|
+
}
|
|
4053
4165
|
return;
|
|
4054
4166
|
}
|
|
4055
4167
|
// telemetry view
|
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.10",
|
|
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": {
|
|
@@ -235,6 +235,21 @@ cmd_query() {
|
|
|
235
235
|
} >&2
|
|
236
236
|
return "$query_rc"
|
|
237
237
|
fi
|
|
238
|
+
# Content-free query telemetry: mode, duration, result count. Never the
|
|
239
|
+
# query text. This is what makes Runtime/QMD retrieval visible in the
|
|
240
|
+
# telemetry event list instead of only legacy graph_query rows.
|
|
241
|
+
local _tele_count _tele_ms
|
|
242
|
+
_tele_ms=$(( ($(date +%s) - t0) * 1000 ))
|
|
243
|
+
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
|
+
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
+
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
248
|
+
else
|
|
249
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" '{mode:$mode, duration_ms:$d}')" 2>/dev/null &
|
|
251
|
+
fi
|
|
252
|
+
|
|
238
253
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
239
254
|
if [ "$context_packet" = 1 ]; then
|
|
240
255
|
local packet
|
|
@@ -59,6 +59,7 @@ settings.sh — Egregore instance settings
|
|
|
59
59
|
settings.sh repo list | add <name> [description] | remove <name>
|
|
60
60
|
settings.sh admin list | add <github-handle> | remove <github-handle>
|
|
61
61
|
settings.sh people list | add <github-handle> | remove <github-handle>
|
|
62
|
+
settings.sh posture status|strict|standard|open org-wide boundary posture
|
|
62
63
|
settings.sh dump full snapshot (JSON)
|
|
63
64
|
|
|
64
65
|
--json machine-readable output for status/list
|
|
@@ -145,7 +146,7 @@ cmd_workflow() {
|
|
|
145
146
|
|| _die "cannot switch workflow: origin/main does not exist or could not be reached" 1
|
|
146
147
|
_save '.base_branch = "main"'
|
|
147
148
|
fi
|
|
148
|
-
echo "Git workflow: SIMPLE — pull requests now target main"
|
|
149
|
+
echo "Git workflow: SIMPLE — pull requests now target main · /save shares it with the org"
|
|
149
150
|
;;
|
|
150
151
|
staged)
|
|
151
152
|
if [ "$base" != "develop" ]; then
|
|
@@ -159,7 +160,7 @@ cmd_workflow() {
|
|
|
159
160
|
elif ! jq -e 'has("base_branch")' "$CONFIG" >/dev/null 2>&1; then
|
|
160
161
|
_save '.base_branch = "develop"'
|
|
161
162
|
fi
|
|
162
|
-
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main"
|
|
163
|
+
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main · /save shares it with the org"
|
|
163
164
|
;;
|
|
164
165
|
*) _die "usage: settings.sh workflow status|simple|staged" 1 ;;
|
|
165
166
|
esac
|
|
@@ -441,6 +442,38 @@ cmd_privacy() {
|
|
|
441
442
|
}'
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
# ── boundary posture (org-wide) ──────────────────────────────────────────
|
|
446
|
+
# Edits committed egregore.json for the whole organization. `locked: true`
|
|
447
|
+
# removes the change path entirely — posture is then org-policy managed.
|
|
448
|
+
|
|
449
|
+
cmd_posture() {
|
|
450
|
+
local action="${1:-status}" current
|
|
451
|
+
current="$(jq -r '.boundary.posture // "standard"' "$CONFIG" 2>/dev/null || echo standard)"
|
|
452
|
+
case "$current" in strict|standard|open) ;; *) current="standard" ;; esac
|
|
453
|
+
case "$action" in
|
|
454
|
+
status)
|
|
455
|
+
if [ "$JSON" = 1 ]; then
|
|
456
|
+
jq -n --arg posture "$current" \
|
|
457
|
+
--argjson locked "$(_boundary_locked && echo true || echo false)" \
|
|
458
|
+
'{domain:"posture", posture:$posture, locked:$locked}'
|
|
459
|
+
else
|
|
460
|
+
echo "Boundary posture: $current"
|
|
461
|
+
_boundary_locked && echo "(organization boundary policy is locked — posture cannot change)"
|
|
462
|
+
fi
|
|
463
|
+
;;
|
|
464
|
+
strict|standard|open)
|
|
465
|
+
if _boundary_locked; then
|
|
466
|
+
_die "boundary policy is managed by your organization and locked; posture cannot change" 1
|
|
467
|
+
fi
|
|
468
|
+
if [ "$action" != "$current" ]; then
|
|
469
|
+
_save '.boundary = ((.boundary // {}) + {posture: $p})' --arg p "$action"
|
|
470
|
+
fi
|
|
471
|
+
echo "Boundary posture: $action · applies to new sessions · /save shares it with the org"
|
|
472
|
+
;;
|
|
473
|
+
*) _die "usage: settings.sh posture status|strict|standard|open" 1 ;;
|
|
474
|
+
esac
|
|
475
|
+
}
|
|
476
|
+
|
|
444
477
|
# ── personal boundary read roots ─────────────────────────────────────────
|
|
445
478
|
# Personal-only: edits .egregore-boundary.local.json for the current member.
|
|
446
479
|
# Respects org `locked: true` (no personal expansion), and never accepts a
|
|
@@ -525,8 +558,9 @@ case "$DOMAIN" in
|
|
|
525
558
|
admin) cmd_admin "$@" ;;
|
|
526
559
|
people) cmd_people "$@" ;;
|
|
527
560
|
privacy) cmd_privacy ;;
|
|
561
|
+
posture) cmd_posture "$@" ;;
|
|
528
562
|
boundary) cmd_boundary "$@" ;;
|
|
529
563
|
dump) cmd_dump ;;
|
|
530
564
|
""|-h|--help|help) usage 0 ;;
|
|
531
|
-
*) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, boundary, dump)" 1 ;;
|
|
565
|
+
*) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, posture, boundary, dump)" 1 ;;
|
|
532
566
|
esac
|
|
@@ -27,6 +27,10 @@ fi
|
|
|
27
27
|
BUFFER_FILE="$BUFFER_DIR/telemetry.jsonl"
|
|
28
28
|
LOCK_DIR="$BUFFER_DIR/telemetry.lock"
|
|
29
29
|
SHARE_CONSENT_FILE="$BUFFER_DIR/telemetry-share-consent.json"
|
|
30
|
+
# Exact dataset pinned at proposal time. The confirm step sends this snapshot,
|
|
31
|
+
# so events emitted between proposal and confirm (attendant, other sessions)
|
|
32
|
+
# can never invalidate — or silently join — a disclosed share.
|
|
33
|
+
SHARE_STAGED_FILE="$BUFFER_DIR/telemetry.share-staged.jsonl"
|
|
30
34
|
STATE_FILE="${EGREGORE_STATE_FILE:-$SCRIPT_DIR/.egregore-state.json}"
|
|
31
35
|
CONFIG="${EGREGORE_CONFIG_FILE:-$SCRIPT_DIR/egregore.json}"
|
|
32
36
|
ENV_FILE="${EGREGORE_ENV_FILE:-$SCRIPT_DIR/.env}"
|
|
@@ -230,7 +234,8 @@ _validate_metrics() {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
_validate_buffer() {
|
|
233
|
-
|
|
237
|
+
local target="${1:-$BUFFER_FILE}"
|
|
238
|
+
[ -s "$target" ] || return 0
|
|
234
239
|
jq -e --arg schema "$SCHEMA_VERSION" --argjson allowed "$ALLOWED_METRICS_JSON" '
|
|
235
240
|
def safe_id:
|
|
236
241
|
type == "string" and length > 0 and length <= 256 and
|
|
@@ -264,7 +269,7 @@ _validate_buffer() {
|
|
|
264
269
|
(.data | safe_metrics) and (.ts | type == "string") and
|
|
265
270
|
(.type | safe_id) and (.sid | safe_id) and (.org | safe_id) and (.user | safe_id)
|
|
266
271
|
end
|
|
267
|
-
' "$
|
|
272
|
+
' "$target" >/dev/null 2>&1
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
cmd_emit() {
|
|
@@ -422,7 +427,7 @@ cmd_clear() {
|
|
|
422
427
|
echo "Could not acquire the telemetry lock; nothing was cleared." >&2
|
|
423
428
|
return 1
|
|
424
429
|
fi
|
|
425
|
-
rm -f "$BUFFER_FILE" "$SHARE_CONSENT_FILE"
|
|
430
|
+
rm -f "$BUFFER_FILE" "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
426
431
|
_unlock
|
|
427
432
|
echo "Local telemetry buffer cleared."
|
|
428
433
|
}
|
|
@@ -484,7 +489,7 @@ _resolve_share_target() {
|
|
|
484
489
|
}
|
|
485
490
|
|
|
486
491
|
cmd_share() {
|
|
487
|
-
local confirmation="" requested_endpoint="" allow_legacy_relay="false"
|
|
492
|
+
local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
|
|
488
493
|
while [ "$#" -gt 0 ]; do
|
|
489
494
|
case "$1" in
|
|
490
495
|
--confirm)
|
|
@@ -499,6 +504,10 @@ cmd_share() {
|
|
|
499
504
|
allow_legacy_relay="true"
|
|
500
505
|
shift
|
|
501
506
|
;;
|
|
507
|
+
--json)
|
|
508
|
+
json_output="true"
|
|
509
|
+
shift
|
|
510
|
+
;;
|
|
502
511
|
*)
|
|
503
512
|
echo "Unknown share option: $1" >&2
|
|
504
513
|
return 2
|
|
@@ -521,31 +530,35 @@ cmd_share() {
|
|
|
521
530
|
https://*|http://127.0.0.1*|http://localhost*|http://\[::1\]*) ;;
|
|
522
531
|
*) echo "Share endpoint must use https (or loopback http)." >&2; return 2 ;;
|
|
523
532
|
esac
|
|
524
|
-
|
|
533
|
+
# Proposal validates the live buffer it is about to stage; confirm validates
|
|
534
|
+
# the staged snapshot it is about to send — never the moving buffer.
|
|
535
|
+
if [ -z "$confirmation" ] && ! _validate_buffer; then
|
|
525
536
|
echo "Share blocked: the local buffer contains an invalid or content-bearing event." >&2
|
|
526
537
|
return 2
|
|
527
538
|
fi
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
count=$(grep -c '^{' "$BUFFER_FILE" 2>/dev/null || true)
|
|
532
|
-
size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
|
|
533
|
-
dataset_sha256=$(_sha256_file "$BUFFER_FILE")
|
|
534
|
-
else
|
|
535
|
-
local empty_file="$BUFFER_DIR/telemetry.empty.$$"
|
|
536
|
-
mkdir -p "$BUFFER_DIR"
|
|
537
|
-
: > "$empty_file"
|
|
538
|
-
dataset_sha256=$(_sha256_file "$empty_file")
|
|
539
|
-
rm -f "$empty_file"
|
|
539
|
+
if [ -n "$confirmation" ] && ! _validate_buffer "$SHARE_STAGED_FILE"; then
|
|
540
|
+
echo "Share blocked: the staged dataset contains an invalid or content-bearing event." >&2
|
|
541
|
+
return 2
|
|
540
542
|
fi
|
|
541
543
|
|
|
544
|
+
local count=0 size=0 dataset_sha256=""
|
|
542
545
|
if [ -z "$confirmation" ]; then
|
|
543
|
-
|
|
546
|
+
mkdir -p "$BUFFER_DIR"
|
|
544
547
|
if [ -s "$BUFFER_FILE" ]; then
|
|
548
|
+
cp "$BUFFER_FILE" "$SHARE_STAGED_FILE"
|
|
549
|
+
else
|
|
550
|
+
: > "$SHARE_STAGED_FILE"
|
|
551
|
+
fi
|
|
552
|
+
chmod 600 "$SHARE_STAGED_FILE"
|
|
553
|
+
count=$(grep -c '^{' "$SHARE_STAGED_FILE" 2>/dev/null || true)
|
|
554
|
+
size=$(wc -c < "$SHARE_STAGED_FILE" 2>/dev/null | tr -d ' ')
|
|
555
|
+
dataset_sha256=$(_sha256_file "$SHARE_STAGED_FILE")
|
|
556
|
+
local fields="none"
|
|
557
|
+
if [ -s "$SHARE_STAGED_FILE" ]; then
|
|
545
558
|
fields=$(jq -rs '
|
|
546
559
|
[.[].metrics // .[].data // {} | keys[]] | unique |
|
|
547
560
|
if length == 0 then "none" else map("metrics." + .) | join(", ") end
|
|
548
|
-
' "$
|
|
561
|
+
' "$SHARE_STAGED_FILE" 2>/dev/null || echo "unable to inspect")
|
|
549
562
|
fi
|
|
550
563
|
local token token_sha256 expires_at
|
|
551
564
|
token=$(_random_token)
|
|
@@ -561,6 +574,25 @@ cmd_share() {
|
|
|
561
574
|
> "$SHARE_CONSENT_FILE"
|
|
562
575
|
chmod 600 "$SHARE_CONSENT_FILE"
|
|
563
576
|
|
|
577
|
+
if [ "$json_output" = "true" ]; then
|
|
578
|
+
# Machine-readable proposal for thin surfaces (the launcher). Same
|
|
579
|
+
# consent protocol: the token here is the only place it exists, and a
|
|
580
|
+
# send still requires a separate explicit `share --confirm`.
|
|
581
|
+
jq -n \
|
|
582
|
+
--arg target "$endpoint" \
|
|
583
|
+
--argjson events "${count:-0}" \
|
|
584
|
+
--argjson bytes "${size:-0}" \
|
|
585
|
+
--arg dataset_sha256 "$dataset_sha256" \
|
|
586
|
+
--arg metric_fields "$fields" \
|
|
587
|
+
--arg token "$token" \
|
|
588
|
+
--argjson expires_at "$expires_at" \
|
|
589
|
+
'{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
|
|
590
|
+
dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
|
|
591
|
+
excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
|
|
592
|
+
token:$token, expires_at:$expires_at}'
|
|
593
|
+
return 0
|
|
594
|
+
fi
|
|
595
|
+
|
|
564
596
|
echo "Telemetry share proposal (nothing sent):"
|
|
565
597
|
echo ""
|
|
566
598
|
echo " Target: $endpoint"
|
|
@@ -577,10 +609,14 @@ cmd_share() {
|
|
|
577
609
|
return 0
|
|
578
610
|
fi
|
|
579
611
|
|
|
580
|
-
if [ ! -f "$SHARE_CONSENT_FILE" ]; then
|
|
612
|
+
if [ ! -f "$SHARE_CONSENT_FILE" ] || [ ! -f "$SHARE_STAGED_FILE" ]; then
|
|
581
613
|
echo "Share blocked: run share without --confirm to inspect and prepare this dataset first." >&2
|
|
582
614
|
return 2
|
|
583
615
|
fi
|
|
616
|
+
# The staged snapshot is the disclosed dataset: exactly what the proposal
|
|
617
|
+
# named is what ships, and later buffer growth cannot invalidate it.
|
|
618
|
+
count=$(grep -c '^{' "$SHARE_STAGED_FILE" 2>/dev/null || true)
|
|
619
|
+
dataset_sha256=$(_sha256_file "$SHARE_STAGED_FILE")
|
|
584
620
|
local expected_target expected_dataset expected_token expires_at actual_token
|
|
585
621
|
expected_target=$(jq -r '.target // empty' "$SHARE_CONSENT_FILE")
|
|
586
622
|
expected_dataset=$(jq -r '.dataset_sha256 // empty' "$SHARE_CONSENT_FILE")
|
|
@@ -588,7 +624,7 @@ cmd_share() {
|
|
|
588
624
|
expires_at=$(jq -r '.expires_at // 0' "$SHARE_CONSENT_FILE")
|
|
589
625
|
actual_token=$(_sha256_text "$confirmation")
|
|
590
626
|
if [ "$(date +%s)" -gt "$expires_at" ] 2>/dev/null; then
|
|
591
|
-
rm -f "$SHARE_CONSENT_FILE"
|
|
627
|
+
rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
592
628
|
echo "Share blocked: confirmation expired; prepare a new share." >&2
|
|
593
629
|
return 2
|
|
594
630
|
fi
|
|
@@ -598,7 +634,7 @@ cmd_share() {
|
|
|
598
634
|
fi
|
|
599
635
|
|
|
600
636
|
local share_file="$BUFFER_DIR/telemetry.share.$$"
|
|
601
|
-
if [ -s "$
|
|
637
|
+
if [ -s "$SHARE_STAGED_FILE" ]; then
|
|
602
638
|
jq -c --arg schema "$SCHEMA_VERSION" '
|
|
603
639
|
if .schema_version == $schema then
|
|
604
640
|
{ts:.occurred_at,type:.event_type,sid:.session_id,org:.org_id,user:.actor_id,
|
|
@@ -606,7 +642,7 @@ cmd_share() {
|
|
|
606
642
|
artifact_ids:.artifact_ids,task_id:.task_id,org_revision:.org_revision,
|
|
607
643
|
index_spec_version:.index_spec_version,shared:true})}
|
|
608
644
|
else . end
|
|
609
|
-
' "$
|
|
645
|
+
' "$SHARE_STAGED_FILE" > "$share_file"
|
|
610
646
|
else
|
|
611
647
|
: > "$share_file"
|
|
612
648
|
fi
|
|
@@ -623,7 +659,7 @@ cmd_share() {
|
|
|
623
659
|
http_code=$(curl "${curl_args[@]}" 2>/dev/null || echo "000")
|
|
624
660
|
rm -f "$share_file"
|
|
625
661
|
if [ "$http_code" -ge 200 ] 2>/dev/null && [ "$http_code" -lt 300 ] 2>/dev/null; then
|
|
626
|
-
rm -f "$SHARE_CONSENT_FILE"
|
|
662
|
+
rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
627
663
|
echo "Shared $count telemetry events with $endpoint. Local events were retained."
|
|
628
664
|
else
|
|
629
665
|
echo "Share failed (HTTP $http_code). Nothing was removed locally." >&2
|
|
@@ -235,6 +235,21 @@ cmd_query() {
|
|
|
235
235
|
} >&2
|
|
236
236
|
return "$query_rc"
|
|
237
237
|
fi
|
|
238
|
+
# Content-free query telemetry: mode, duration, result count. Never the
|
|
239
|
+
# query text. This is what makes Runtime/QMD retrieval visible in the
|
|
240
|
+
# telemetry event list instead of only legacy graph_query rows.
|
|
241
|
+
local _tele_count _tele_ms
|
|
242
|
+
_tele_ms=$(( ($(date +%s) - t0) * 1000 ))
|
|
243
|
+
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
|
+
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
+
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
248
|
+
else
|
|
249
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" '{mode:$mode, duration_ms:$d}')" 2>/dev/null &
|
|
251
|
+
fi
|
|
252
|
+
|
|
238
253
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
239
254
|
if [ "$context_packet" = 1 ]; then
|
|
240
255
|
local packet
|
|
@@ -59,6 +59,7 @@ settings.sh — Egregore instance settings
|
|
|
59
59
|
settings.sh repo list | add <name> [description] | remove <name>
|
|
60
60
|
settings.sh admin list | add <github-handle> | remove <github-handle>
|
|
61
61
|
settings.sh people list | add <github-handle> | remove <github-handle>
|
|
62
|
+
settings.sh posture status|strict|standard|open org-wide boundary posture
|
|
62
63
|
settings.sh dump full snapshot (JSON)
|
|
63
64
|
|
|
64
65
|
--json machine-readable output for status/list
|
|
@@ -145,7 +146,7 @@ cmd_workflow() {
|
|
|
145
146
|
|| _die "cannot switch workflow: origin/main does not exist or could not be reached" 1
|
|
146
147
|
_save '.base_branch = "main"'
|
|
147
148
|
fi
|
|
148
|
-
echo "Git workflow: SIMPLE — pull requests now target main"
|
|
149
|
+
echo "Git workflow: SIMPLE — pull requests now target main · /save shares it with the org"
|
|
149
150
|
;;
|
|
150
151
|
staged)
|
|
151
152
|
if [ "$base" != "develop" ]; then
|
|
@@ -159,7 +160,7 @@ cmd_workflow() {
|
|
|
159
160
|
elif ! jq -e 'has("base_branch")' "$CONFIG" >/dev/null 2>&1; then
|
|
160
161
|
_save '.base_branch = "develop"'
|
|
161
162
|
fi
|
|
162
|
-
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main"
|
|
163
|
+
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main · /save shares it with the org"
|
|
163
164
|
;;
|
|
164
165
|
*) _die "usage: settings.sh workflow status|simple|staged" 1 ;;
|
|
165
166
|
esac
|
|
@@ -441,6 +442,38 @@ cmd_privacy() {
|
|
|
441
442
|
}'
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
# ── boundary posture (org-wide) ──────────────────────────────────────────
|
|
446
|
+
# Edits committed egregore.json for the whole organization. `locked: true`
|
|
447
|
+
# removes the change path entirely — posture is then org-policy managed.
|
|
448
|
+
|
|
449
|
+
cmd_posture() {
|
|
450
|
+
local action="${1:-status}" current
|
|
451
|
+
current="$(jq -r '.boundary.posture // "standard"' "$CONFIG" 2>/dev/null || echo standard)"
|
|
452
|
+
case "$current" in strict|standard|open) ;; *) current="standard" ;; esac
|
|
453
|
+
case "$action" in
|
|
454
|
+
status)
|
|
455
|
+
if [ "$JSON" = 1 ]; then
|
|
456
|
+
jq -n --arg posture "$current" \
|
|
457
|
+
--argjson locked "$(_boundary_locked && echo true || echo false)" \
|
|
458
|
+
'{domain:"posture", posture:$posture, locked:$locked}'
|
|
459
|
+
else
|
|
460
|
+
echo "Boundary posture: $current"
|
|
461
|
+
_boundary_locked && echo "(organization boundary policy is locked — posture cannot change)"
|
|
462
|
+
fi
|
|
463
|
+
;;
|
|
464
|
+
strict|standard|open)
|
|
465
|
+
if _boundary_locked; then
|
|
466
|
+
_die "boundary policy is managed by your organization and locked; posture cannot change" 1
|
|
467
|
+
fi
|
|
468
|
+
if [ "$action" != "$current" ]; then
|
|
469
|
+
_save '.boundary = ((.boundary // {}) + {posture: $p})' --arg p "$action"
|
|
470
|
+
fi
|
|
471
|
+
echo "Boundary posture: $action · applies to new sessions · /save shares it with the org"
|
|
472
|
+
;;
|
|
473
|
+
*) _die "usage: settings.sh posture status|strict|standard|open" 1 ;;
|
|
474
|
+
esac
|
|
475
|
+
}
|
|
476
|
+
|
|
444
477
|
# ── personal boundary read roots ─────────────────────────────────────────
|
|
445
478
|
# Personal-only: edits .egregore-boundary.local.json for the current member.
|
|
446
479
|
# Respects org `locked: true` (no personal expansion), and never accepts a
|
|
@@ -525,8 +558,9 @@ case "$DOMAIN" in
|
|
|
525
558
|
admin) cmd_admin "$@" ;;
|
|
526
559
|
people) cmd_people "$@" ;;
|
|
527
560
|
privacy) cmd_privacy ;;
|
|
561
|
+
posture) cmd_posture "$@" ;;
|
|
528
562
|
boundary) cmd_boundary "$@" ;;
|
|
529
563
|
dump) cmd_dump ;;
|
|
530
564
|
""|-h|--help|help) usage 0 ;;
|
|
531
|
-
*) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, boundary, dump)" 1 ;;
|
|
565
|
+
*) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, posture, boundary, dump)" 1 ;;
|
|
532
566
|
esac
|