create-egregore 0.21.0-runtime-mvp.8 → 0.21.0-runtime-mvp.9

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.21.0-runtime-mvp.8
2
+ // egregore-launcher-version: 0.21.0-runtime-mvp.9
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.8";
38
+ const VERSION = "0.21.0-runtime-mvp.9";
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 shareLines = (data && data.shareLines) || ["share proposal unavailable"];
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
- for (const line of shareLines.slice(0, 16)) {
3215
- lines.push(boxLine(`${line.includes("--confirm") ? "" : DIM}${line}${RESET}`, innerWidth, margin));
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(boxLine(`${DIM}Sharing completes only by running the confirm command above in a terminal. ← back${RESET}`, innerWidth, margin));
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
- const r = runTelemetry(S.settingsInst, ["share"]);
3970
- const shareLines = ((r && (r.stdout || r.stderr)) || "share preview unavailable").split("\n").filter(Boolean);
3971
- S.settingsData = { ...(S.settingsData || {}), shareLines };
3972
- S.settingsMode = "shareinfo"; S.settingsNotice = "";
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
- if (key.name === "escape" || key.name === "left") { S.settingsMode = "events"; render(); return; }
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) to ${proposal.target} 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.8",
3
+ "version": "0.21.0-runtime-mvp.9",
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": {
@@ -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
@@ -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"
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
@@ -484,7 +484,7 @@ _resolve_share_target() {
484
484
  }
485
485
 
486
486
  cmd_share() {
487
- local confirmation="" requested_endpoint="" allow_legacy_relay="false"
487
+ local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
488
488
  while [ "$#" -gt 0 ]; do
489
489
  case "$1" in
490
490
  --confirm)
@@ -499,6 +499,10 @@ cmd_share() {
499
499
  allow_legacy_relay="true"
500
500
  shift
501
501
  ;;
502
+ --json)
503
+ json_output="true"
504
+ shift
505
+ ;;
502
506
  *)
503
507
  echo "Unknown share option: $1" >&2
504
508
  return 2
@@ -561,6 +565,25 @@ cmd_share() {
561
565
  > "$SHARE_CONSENT_FILE"
562
566
  chmod 600 "$SHARE_CONSENT_FILE"
563
567
 
568
+ if [ "$json_output" = "true" ]; then
569
+ # Machine-readable proposal for thin surfaces (the launcher). Same
570
+ # consent protocol: the token here is the only place it exists, and a
571
+ # send still requires a separate explicit `share --confirm`.
572
+ jq -n \
573
+ --arg target "$endpoint" \
574
+ --argjson events "${count:-0}" \
575
+ --argjson bytes "${size:-0}" \
576
+ --arg dataset_sha256 "$dataset_sha256" \
577
+ --arg metric_fields "$fields" \
578
+ --arg token "$token" \
579
+ --argjson expires_at "$expires_at" \
580
+ '{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
581
+ dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
582
+ excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
583
+ token:$token, expires_at:$expires_at}'
584
+ return 0
585
+ fi
586
+
564
587
  echo "Telemetry share proposal (nothing sent):"
565
588
  echo ""
566
589
  echo " Target: $endpoint"
@@ -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
@@ -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"
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
@@ -484,7 +484,7 @@ _resolve_share_target() {
484
484
  }
485
485
 
486
486
  cmd_share() {
487
- local confirmation="" requested_endpoint="" allow_legacy_relay="false"
487
+ local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
488
488
  while [ "$#" -gt 0 ]; do
489
489
  case "$1" in
490
490
  --confirm)
@@ -499,6 +499,10 @@ cmd_share() {
499
499
  allow_legacy_relay="true"
500
500
  shift
501
501
  ;;
502
+ --json)
503
+ json_output="true"
504
+ shift
505
+ ;;
502
506
  *)
503
507
  echo "Unknown share option: $1" >&2
504
508
  return 2
@@ -561,6 +565,25 @@ cmd_share() {
561
565
  > "$SHARE_CONSENT_FILE"
562
566
  chmod 600 "$SHARE_CONSENT_FILE"
563
567
 
568
+ if [ "$json_output" = "true" ]; then
569
+ # Machine-readable proposal for thin surfaces (the launcher). Same
570
+ # consent protocol: the token here is the only place it exists, and a
571
+ # send still requires a separate explicit `share --confirm`.
572
+ jq -n \
573
+ --arg target "$endpoint" \
574
+ --argjson events "${count:-0}" \
575
+ --argjson bytes "${size:-0}" \
576
+ --arg dataset_sha256 "$dataset_sha256" \
577
+ --arg metric_fields "$fields" \
578
+ --arg token "$token" \
579
+ --argjson expires_at "$expires_at" \
580
+ '{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
581
+ dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
582
+ excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
583
+ token:$token, expires_at:$expires_at}'
584
+ return 0
585
+ fi
586
+
564
587
  echo "Telemetry share proposal (nothing sent):"
565
588
  echo ""
566
589
  echo " Target: $endpoint"
@@ -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
@@ -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"
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
@@ -484,7 +484,7 @@ _resolve_share_target() {
484
484
  }
485
485
 
486
486
  cmd_share() {
487
- local confirmation="" requested_endpoint="" allow_legacy_relay="false"
487
+ local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
488
488
  while [ "$#" -gt 0 ]; do
489
489
  case "$1" in
490
490
  --confirm)
@@ -499,6 +499,10 @@ cmd_share() {
499
499
  allow_legacy_relay="true"
500
500
  shift
501
501
  ;;
502
+ --json)
503
+ json_output="true"
504
+ shift
505
+ ;;
502
506
  *)
503
507
  echo "Unknown share option: $1" >&2
504
508
  return 2
@@ -561,6 +565,25 @@ cmd_share() {
561
565
  > "$SHARE_CONSENT_FILE"
562
566
  chmod 600 "$SHARE_CONSENT_FILE"
563
567
 
568
+ if [ "$json_output" = "true" ]; then
569
+ # Machine-readable proposal for thin surfaces (the launcher). Same
570
+ # consent protocol: the token here is the only place it exists, and a
571
+ # send still requires a separate explicit `share --confirm`.
572
+ jq -n \
573
+ --arg target "$endpoint" \
574
+ --argjson events "${count:-0}" \
575
+ --argjson bytes "${size:-0}" \
576
+ --arg dataset_sha256 "$dataset_sha256" \
577
+ --arg metric_fields "$fields" \
578
+ --arg token "$token" \
579
+ --argjson expires_at "$expires_at" \
580
+ '{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
581
+ dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
582
+ excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
583
+ token:$token, expires_at:$expires_at}'
584
+ return 0
585
+ fi
586
+
564
587
  echo "Telemetry share proposal (nothing sent):"
565
588
  echo ""
566
589
  echo " Target: $endpoint"
@@ -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
@@ -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"
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
@@ -484,7 +484,7 @@ _resolve_share_target() {
484
484
  }
485
485
 
486
486
  cmd_share() {
487
- local confirmation="" requested_endpoint="" allow_legacy_relay="false"
487
+ local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
488
488
  while [ "$#" -gt 0 ]; do
489
489
  case "$1" in
490
490
  --confirm)
@@ -499,6 +499,10 @@ cmd_share() {
499
499
  allow_legacy_relay="true"
500
500
  shift
501
501
  ;;
502
+ --json)
503
+ json_output="true"
504
+ shift
505
+ ;;
502
506
  *)
503
507
  echo "Unknown share option: $1" >&2
504
508
  return 2
@@ -561,6 +565,25 @@ cmd_share() {
561
565
  > "$SHARE_CONSENT_FILE"
562
566
  chmod 600 "$SHARE_CONSENT_FILE"
563
567
 
568
+ if [ "$json_output" = "true" ]; then
569
+ # Machine-readable proposal for thin surfaces (the launcher). Same
570
+ # consent protocol: the token here is the only place it exists, and a
571
+ # send still requires a separate explicit `share --confirm`.
572
+ jq -n \
573
+ --arg target "$endpoint" \
574
+ --argjson events "${count:-0}" \
575
+ --argjson bytes "${size:-0}" \
576
+ --arg dataset_sha256 "$dataset_sha256" \
577
+ --arg metric_fields "$fields" \
578
+ --arg token "$token" \
579
+ --argjson expires_at "$expires_at" \
580
+ '{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
581
+ dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
582
+ excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
583
+ token:$token, expires_at:$expires_at}'
584
+ return 0
585
+ fi
586
+
564
587
  echo "Telemetry share proposal (nothing sent):"
565
588
  echo ""
566
589
  echo " Target: $endpoint"