@mrciphersmith/keryx 0.3.67 → 0.3.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20107,7 +20107,7 @@ var package_default;
20107
20107
  var init_package = __esm(() => {
20108
20108
  package_default = {
20109
20109
  name: "@mrciphersmith/keryx",
20110
- version: "0.3.67",
20110
+ version: "0.3.68",
20111
20111
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20112
20112
  private: false,
20113
20113
  publishConfig: {
@@ -81424,7 +81424,7 @@ ${shown.join(`
81424
81424
  result = precomputedResult;
81425
81425
  } else {
81426
81426
  try {
81427
- result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks, untrustedOrigin, io.trustedMcpTools, io.mcpToolFingerprint, io.mcpToolDestructive, deps.unattended === true ? undefined : io.beforeMutation);
81427
+ result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks, untrustedOrigin, io.mcpGrantsApply?.() === false ? undefined : io.trustedMcpTools, io.mcpToolFingerprint, io.mcpToolDestructive, deps.unattended === true ? undefined : io.beforeMutation);
81428
81428
  } catch (err) {
81429
81429
  const message = err instanceof Error ? err.message : String(err);
81430
81430
  result = { output: `${call.name} failed: ${message}`, isError: true };
@@ -91621,6 +91621,34 @@ function allowShellPattern(pattern, dir) {
91621
91621
  }
91622
91622
  return trimmed;
91623
91623
  }
91624
+ function removeShellPattern(pattern, dir) {
91625
+ const wanted = pattern.trim();
91626
+ if (wanted.length === 0)
91627
+ return false;
91628
+ try {
91629
+ const file = shellPermissionsPath(dir);
91630
+ if (!existsSync36(file))
91631
+ return false;
91632
+ const read = readConfigFile(file);
91633
+ if (!read.ok)
91634
+ return false;
91635
+ const raw = JSON.parse(read.text);
91636
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
91637
+ return false;
91638
+ const allowRaw = raw.allow;
91639
+ if (!Array.isArray(allowRaw))
91640
+ return false;
91641
+ const kept = allowRaw.filter((entry) => !(typeof entry === "string" && entry.trim() === wanted));
91642
+ if (kept.length === allowRaw.length)
91643
+ return false;
91644
+ ensureKeryxConfigDir(path212.dirname(file));
91645
+ writeOwnerOnlyFile(file, `${JSON.stringify({ ...raw, allow: kept }, null, 2)}
91646
+ `);
91647
+ return true;
91648
+ } catch {
91649
+ return false;
91650
+ }
91651
+ }
91624
91652
  function matchShellPattern(pattern, command) {
91625
91653
  const p = pattern.trim();
91626
91654
  const c = command.trim();
@@ -91844,7 +91872,16 @@ function evaluateShellApproval(input) {
91844
91872
  const hookAsk = input.meta?.hookAsk === true;
91845
91873
  const untrustedOrigin = input.meta?.untrustedOrigin === true;
91846
91874
  const audit = io.loadAudit();
91847
- for (const pattern of audit.permissions.allow) {
91875
+ const current = new Set(audit.permissions.allow);
91876
+ const loadedBefore = loadedFromDisk.get(input.sessionAllow);
91877
+ if (loadedBefore !== undefined) {
91878
+ for (const pattern of loadedBefore) {
91879
+ if (!current.has(pattern))
91880
+ input.sessionAllow.delete(pattern);
91881
+ }
91882
+ }
91883
+ loadedFromDisk.set(input.sessionAllow, current);
91884
+ for (const pattern of current) {
91848
91885
  input.sessionAllow.add(pattern);
91849
91886
  }
91850
91887
  const tampered = io.fingerprint() !== input.fingerprintAtStart;
@@ -91899,7 +91936,7 @@ function formatShellApprovalHints(evaled) {
91899
91936
  }
91900
91937
  return lines;
91901
91938
  }
91902
- var defaultApprovalIO;
91939
+ var defaultApprovalIO, loadedFromDisk;
91903
91940
  var init_shell_approval = __esm(() => {
91904
91941
  init_command_risk();
91905
91942
  init_shell_permissions();
@@ -91907,6 +91944,7 @@ var init_shell_approval = __esm(() => {
91907
91944
  loadAudit: () => loadShellPermissionsWithAudit(),
91908
91945
  fingerprint: () => shellPermissionsFingerprint()
91909
91946
  };
91947
+ loadedFromDisk = new WeakMap;
91910
91948
  });
91911
91949
 
91912
91950
  // src/mcp-servers/json-utils.ts
@@ -101030,9 +101068,34 @@ function parseRemoteConfig(value) {
101030
101068
  if (!orphan.ok) {
101031
101069
  return orphan;
101032
101070
  }
101033
- const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
101034
- if (!run.ok) {
101035
- return run;
101071
+ let runTimeoutMs = DEFAULT_RUN_TIMEOUT_MS;
101072
+ if (doc.runTimeoutMs !== undefined) {
101073
+ if (doc.runTimeoutMs === 0) {
101074
+ runTimeoutMs = 0;
101075
+ } else {
101076
+ const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
101077
+ if (!run.ok) {
101078
+ return { ok: false, reason: `remote config runTimeoutMs must be 0 (no limit) or an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
101079
+ }
101080
+ runTimeoutMs = run.value;
101081
+ }
101082
+ }
101083
+ let permissionMode = DEFAULT_REMOTE_PERMISSION_MODE;
101084
+ if (doc.permissionMode !== undefined) {
101085
+ if (doc.permissionMode !== "ask" && doc.permissionMode !== "trust") {
101086
+ return { ok: false, reason: 'remote config permissionMode must be "ask" or "trust" (auto is not available here)' };
101087
+ }
101088
+ permissionMode = doc.permissionMode;
101089
+ }
101090
+ let approvalTimeoutMs = DEFAULT_APPROVAL_WAIT_MS;
101091
+ if (doc.approvalTimeoutMs !== undefined) {
101092
+ if (!isSafeInt(doc.approvalTimeoutMs) || doc.approvalTimeoutMs < MIN_APPROVAL_WAIT_MS || doc.approvalTimeoutMs > MAX_APPROVAL_WAIT_MS) {
101093
+ return {
101094
+ ok: false,
101095
+ reason: `remote config approvalTimeoutMs must be an integer from ${MIN_APPROVAL_WAIT_MS} to ${MAX_APPROVAL_WAIT_MS} (milliseconds)`
101096
+ };
101097
+ }
101098
+ approvalTimeoutMs = doc.approvalTimeoutMs;
101036
101099
  }
101037
101100
  if (doc.rendering !== undefined && !isRenderMode(doc.rendering)) {
101038
101101
  return { ok: false, reason: `remote config rendering must be ${RENDER_MODE_CHOICES}` };
@@ -101044,7 +101107,9 @@ function parseRemoteConfig(value) {
101044
101107
  chatId: doc.chatId,
101045
101108
  allowedUserIds: [...new Set(ids)],
101046
101109
  orphanMs: orphan.value,
101047
- runTimeoutMs: run.value,
101110
+ runTimeoutMs,
101111
+ permissionMode,
101112
+ approvalTimeoutMs,
101048
101113
  ...doc.rendering === undefined ? {} : { rendering: doc.rendering }
101049
101114
  }
101050
101115
  };
@@ -101072,7 +101137,46 @@ function saveRemoteConfig(config, dir) {
101072
101137
  return checked;
101073
101138
  }
101074
101139
  ensureRemoteDir(dir);
101075
- writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(checked.value, null, 2)}
101140
+ const { permissionMode, approvalTimeoutMs, ...base } = checked.value;
101141
+ const onDisk = {
101142
+ ...base,
101143
+ ...config.permissionMode === undefined ? {} : { permissionMode },
101144
+ ...config.approvalTimeoutMs === undefined ? {} : { approvalTimeoutMs }
101145
+ };
101146
+ writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(onDisk, null, 2)}
101147
+ `);
101148
+ return checked;
101149
+ }
101150
+ function updateRemotePolicy(patch, dir) {
101151
+ const file = remoteConfigPath(dir);
101152
+ const read = readConfigFile(file);
101153
+ if (!read.ok) {
101154
+ return {
101155
+ ok: false,
101156
+ reason: read.reason === "absent" ? `no remote-control config at ${file}; connect Telegram first` : `remote-control config at ${file} cannot be read (${read.reason})`
101157
+ };
101158
+ }
101159
+ let doc;
101160
+ try {
101161
+ doc = JSON.parse(read.text);
101162
+ } catch {
101163
+ return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
101164
+ }
101165
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
101166
+ return { ok: false, reason: "remote config must be a JSON object" };
101167
+ }
101168
+ const next = { ...doc };
101169
+ if (patch.permissionMode !== undefined)
101170
+ next.permissionMode = patch.permissionMode;
101171
+ if (patch.runTimeoutMs !== undefined)
101172
+ next.runTimeoutMs = patch.runTimeoutMs;
101173
+ if (patch.approvalTimeoutMs !== undefined)
101174
+ next.approvalTimeoutMs = patch.approvalTimeoutMs;
101175
+ const checked = parseRemoteConfig(next);
101176
+ if (!checked.ok) {
101177
+ return checked;
101178
+ }
101179
+ writeOwnerOnlyFileAtomic(file, `${JSON.stringify(next, null, 2)}
101076
101180
  `);
101077
101181
  return checked;
101078
101182
  }
@@ -101139,15 +101243,27 @@ function removeBotToken(dir) {
101139
101243
  function removeRemoteConfig(dir) {
101140
101244
  return removeFile2(remoteConfigPath(dir));
101141
101245
  }
101142
- var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS, TOKEN_SHAPE;
101246
+ var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS = 0, LEGACY_RUN_TIMEOUT_MS, DEFAULT_APPROVAL_WAIT_MS, LEGACY_APPROVAL_WAIT_MS, MIN_APPROVAL_WAIT_MS = 30000, MAX_APPROVAL_WAIT_MS, DEFAULT_REMOTE_PERMISSION_MODE = "trust", MAX_TIMEOUT_MS3, CONFIG_KEYS, TOKEN_SHAPE;
101143
101247
  var init_config11 = __esm(() => {
101144
101248
  init_config_dir();
101145
101249
  init_paths4();
101146
101250
  init_rendering_mode();
101147
101251
  DEFAULT_ORPHAN_MS = 10 * 60000;
101148
- DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
101252
+ LEGACY_RUN_TIMEOUT_MS = 30 * 60000;
101253
+ DEFAULT_APPROVAL_WAIT_MS = 15 * 60000;
101254
+ LEGACY_APPROVAL_WAIT_MS = 5 * 60000;
101255
+ MAX_APPROVAL_WAIT_MS = 60 * 60000;
101149
101256
  MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
101150
- CONFIG_KEYS = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs", "rendering"]);
101257
+ CONFIG_KEYS = new Set([
101258
+ "schemaVersion",
101259
+ "chatId",
101260
+ "allowedUserIds",
101261
+ "orphanMs",
101262
+ "runTimeoutMs",
101263
+ "permissionMode",
101264
+ "approvalTimeoutMs",
101265
+ "rendering"
101266
+ ]);
101151
101267
  TOKEN_SHAPE = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
101152
101268
  });
101153
101269
 
@@ -101219,6 +101335,308 @@ var init_rendering_command = __esm(() => {
101219
101335
  };
101220
101336
  });
101221
101337
 
101338
+ // src/commands/permissions-command.ts
101339
+ function loadPermissionsView(options = {}) {
101340
+ const audit = loadShellPermissionsWithAudit(options.dir);
101341
+ const stored = new Set([...audit.permissions.allow, ...audit.rejected.map((entry) => entry.pattern)]);
101342
+ const rows = [];
101343
+ const push = (pattern, kind, reason) => {
101344
+ rows.push({ n: rows.length + 1, pattern, kind, ...reason !== undefined ? { reason } : {} });
101345
+ };
101346
+ for (const pattern of audit.permissions.allow)
101347
+ push(pattern, "active");
101348
+ audit.rejected.forEach((entry) => push(entry.pattern, "inactive", entry.reason));
101349
+ for (const pattern of options.sessionAllow ?? []) {
101350
+ if (!stored.has(pattern))
101351
+ push(pattern, "session");
101352
+ }
101353
+ return { path: shellPermissionsPath(options.dir), rows };
101354
+ }
101355
+ function patternOnOneLine(pattern) {
101356
+ return pattern.replace(/\r?\n/g, "\\n").replace(CONTROL, " ");
101357
+ }
101358
+ function permissionsLines(view) {
101359
+ const of = (kind) => view.rows.filter((row) => row.kind === kind);
101360
+ const active = of("active");
101361
+ const inactive = of("inactive");
101362
+ const session = of("session");
101363
+ const lines = [];
101364
+ lines.push(active.length === 0 ? "No saved shell rules: every command that is not read-only asks first." : `Saved shell rules (${active.length}): commands that run without asking.`);
101365
+ for (const row of active)
101366
+ lines.push(` ${row.n}. ${patternOnOneLine(row.pattern)}`);
101367
+ if (inactive.length > 0) {
101368
+ lines.push("", `Not honoured (${inactive.length}): kept in the file, they approve nothing.`);
101369
+ for (const row of inactive) {
101370
+ lines.push(` ${row.n}. ${patternOnOneLine(row.pattern)}`);
101371
+ lines.push(` ${row.reason ?? "refused by the validators"}`);
101372
+ }
101373
+ }
101374
+ if (session.length > 0) {
101375
+ lines.push("", `Granted for this session only (${session.length}):`);
101376
+ for (const row of session)
101377
+ lines.push(` ${row.n}. ${patternOnOneLine(row.pattern)}`);
101378
+ }
101379
+ lines.push("", `File: ${view.path}`);
101380
+ if (view.rows.length > 0)
101381
+ lines.push("Remove one with /permissions remove <number>, or keryx permissions remove <number>.");
101382
+ return lines;
101383
+ }
101384
+ function resolvePermissionTarget(view, target) {
101385
+ const wanted = target.trim();
101386
+ if (wanted.length === 0)
101387
+ return;
101388
+ const byText = view.rows.find((row) => row.pattern === wanted);
101389
+ if (byText !== undefined)
101390
+ return byText;
101391
+ if (/^\d+$/.test(wanted))
101392
+ return view.rows.find((row) => row.n === Number(wanted));
101393
+ return;
101394
+ }
101395
+ function removePermission(target, options = {}) {
101396
+ const view = loadPermissionsView({
101397
+ ...options.dir !== undefined ? { dir: options.dir } : {},
101398
+ ...options.sessionAllow !== undefined ? { sessionAllow: options.sessionAllow } : {}
101399
+ });
101400
+ const row = resolvePermissionTarget(view, target);
101401
+ if (row === undefined) {
101402
+ return { ok: false, text: `No saved rule matches ${JSON.stringify(target.trim())}. Run list to see the numbers.` };
101403
+ }
101404
+ if (row.kind === "session") {
101405
+ options.sessionAllow?.delete(row.pattern);
101406
+ return { ok: true, pattern: row.pattern, text: `Removed the session grant: ${patternOnOneLine(row.pattern)}` };
101407
+ }
101408
+ if (!removeShellPattern(row.pattern, options.dir)) {
101409
+ return { ok: false, pattern: row.pattern, text: `Could not remove ${patternOnOneLine(row.pattern)} from ${view.path}.` };
101410
+ }
101411
+ options.sessionAllow?.delete(row.pattern);
101412
+ options.onChanged?.();
101413
+ return { ok: true, pattern: row.pattern, text: `Removed: ${patternOnOneLine(row.pattern)}. It asks again from now on.` };
101414
+ }
101415
+ function permissionsSlashText(rest, options = {}) {
101416
+ const trimmed = rest.trim();
101417
+ const [sub] = trimmed.split(/\s+/);
101418
+ if (sub === "remove" || sub === "rm") {
101419
+ const target = trimmed.slice(sub.length).trim();
101420
+ if (target.length === 0)
101421
+ return `Usage: /permissions remove <number | pattern>
101422
+ `;
101423
+ return `${removePermission(target, options).text}
101424
+ `;
101425
+ }
101426
+ if (trimmed.length > 0 && sub !== "list")
101427
+ return `Usage: /permissions [list | remove <number | pattern>]
101428
+ `;
101429
+ return `${permissionsLines(loadPermissionsView({
101430
+ ...options.dir !== undefined ? { dir: options.dir } : {},
101431
+ ...options.sessionAllow !== undefined ? { sessionAllow: options.sessionAllow } : {}
101432
+ })).join(`
101433
+ `)}
101434
+ `;
101435
+ }
101436
+ function printPermissionsHelp() {
101437
+ console.log([
101438
+ "Usage: keryx permissions <subcommand>",
101439
+ "",
101440
+ " keryx permissions list [--json] The saved shell rules: honoured, and not honoured with the reason.",
101441
+ " keryx permissions remove <number|pattern>",
101442
+ " Take one rule back. The number is the one `list` prints.",
101443
+ "",
101444
+ "A rule is saved when an approval is answered with Always, in the shell or from Telegram. It",
101445
+ "is removed here, never from a chat. A running shell stops using a removed rule on its next",
101446
+ "approval and warns that the file changed."
101447
+ ].join(`
101448
+ `));
101449
+ }
101450
+ async function permissionsCommand(args) {
101451
+ const [sub, ...rest] = args;
101452
+ if (sub === "--help" || sub === "-h" || sub === "help") {
101453
+ printPermissionsHelp();
101454
+ return;
101455
+ }
101456
+ if (sub === undefined || sub === "list" || sub === "--json") {
101457
+ const flags = sub === "--json" ? [sub, ...rest] : rest;
101458
+ const unknown = flags.filter((arg) => arg !== "--json");
101459
+ if (unknown.length > 0) {
101460
+ throw new Error(`Unknown option${unknown.length > 1 ? "s" : ""} for \`keryx permissions list\`: ${unknown.join(", ")}. Accepted: --json.`);
101461
+ }
101462
+ const view = loadPermissionsView();
101463
+ if (flags.includes("--json")) {
101464
+ console.log(JSON.stringify({
101465
+ path: view.path,
101466
+ rules: view.rows.map((row) => ({
101467
+ n: row.n,
101468
+ pattern: row.pattern,
101469
+ honoured: row.kind === "active",
101470
+ ...row.reason !== undefined ? { reason: row.reason } : {}
101471
+ }))
101472
+ }, null, 2));
101473
+ return;
101474
+ }
101475
+ console.log(permissionsLines(view).join(`
101476
+ `));
101477
+ return;
101478
+ }
101479
+ if (sub === "remove" || sub === "rm") {
101480
+ const target = rest.join(" ").trim();
101481
+ if (target.length === 0 || target.startsWith("--")) {
101482
+ throw new Error("Usage: keryx permissions remove <number | pattern>");
101483
+ }
101484
+ const result = removePermission(target);
101485
+ if (!result.ok)
101486
+ throw new Error(result.text);
101487
+ console.log(result.text);
101488
+ return;
101489
+ }
101490
+ throw new Error(`Unknown permissions subcommand: ${sub}. Run \`keryx permissions --help\`.`);
101491
+ }
101492
+ var CONTROL;
101493
+ var init_permissions_command = __esm(() => {
101494
+ init_shell_permissions();
101495
+ CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
101496
+ });
101497
+
101498
+ // src/commands/remote-policy-command.ts
101499
+ function formatPolicyDuration(ms) {
101500
+ if (ms <= 0)
101501
+ return "none";
101502
+ const seconds = Math.round(ms / 1000);
101503
+ if (seconds < 60)
101504
+ return `${seconds}s`;
101505
+ const minutes = Math.floor(seconds / 60);
101506
+ const rest = seconds % 60;
101507
+ if (rest !== 0)
101508
+ return `${minutes}m ${rest}s`;
101509
+ if (minutes >= 60 && minutes % 60 === 0)
101510
+ return `${minutes / 60}h`;
101511
+ return `${minutes}m`;
101512
+ }
101513
+ function countSavedRules(dir) {
101514
+ try {
101515
+ return loadPermissionsView(dir !== undefined ? { dir } : {}).rows.filter((row) => row.kind === "active").length;
101516
+ } catch {
101517
+ return 0;
101518
+ }
101519
+ }
101520
+ function postureLines(posture) {
101521
+ const lines = [
101522
+ `Mode: ${posture.defaultMode} - a Telegram turn starts with it until /mode changes the shell's mode`,
101523
+ `Run limit: ${formatPolicyDuration(posture.runTimeoutMs)}${posture.runTimeoutMs === 0 ? " (/stop ends a run)" : ""}`,
101524
+ `Approval wait: ${formatPolicyDuration(posture.approvalTimeoutMs)}`,
101525
+ `Saved shell rules: ${posture.savedRules} (${"/permissions"} lists and removes them)`
101526
+ ];
101527
+ if (posture.inForce !== undefined)
101528
+ lines.unshift(`In force now: ${posture.inForce}`);
101529
+ return lines;
101530
+ }
101531
+ function postureSidebarText(posture) {
101532
+ const limit = posture.runTimeoutMs > 0 ? `limit ${formatPolicyDuration(posture.runTimeoutMs)}` : "no limit";
101533
+ const first = `${posture.defaultMode} \xB7 ${limit} \xB7 wait ${formatPolicyDuration(posture.approvalTimeoutMs)}`;
101534
+ return posture.savedRules > 0 ? `${first}
101535
+ ${posture.savedRules} saved shell rule${posture.savedRules === 1 ? "" : "s"}` : first;
101536
+ }
101537
+ function wholeMinutes(word) {
101538
+ if (word === undefined || !/^\d{1,6}$/.test(word))
101539
+ return;
101540
+ return Number(word);
101541
+ }
101542
+ function parseRemotePolicyArgs(rest) {
101543
+ const words = rest.trim().split(/\s+/).filter((word) => word.length > 0);
101544
+ if (words.length === 0)
101545
+ return { action: "show" };
101546
+ const patch = {};
101547
+ let index = 0;
101548
+ while (index < words.length) {
101549
+ const key = (words[index] ?? "").toLowerCase();
101550
+ const value = words[index + 1];
101551
+ if (value === undefined)
101552
+ return { action: "invalid", message: REMOTE_POLICY_USAGE };
101553
+ const lower = value.toLowerCase();
101554
+ if (key === "mode") {
101555
+ if (patch.permissionMode !== undefined)
101556
+ return { action: "invalid", message: REMOTE_POLICY_USAGE };
101557
+ if (lower === "auto") {
101558
+ return { action: "invalid", message: "auto is not available as a Telegram default: it skips every confirmation. Use /mode auto in the shell if you mean it." };
101559
+ }
101560
+ if (lower !== "ask" && lower !== "trust")
101561
+ return { action: "invalid", message: `Unknown mode '${value}'. Choose ask or trust.` };
101562
+ patch.permissionMode = lower;
101563
+ } else if (key === "limit") {
101564
+ if (patch.runTimeoutMs !== undefined)
101565
+ return { action: "invalid", message: REMOTE_POLICY_USAGE };
101566
+ if (lower === "none" || lower === "off" || lower === "0") {
101567
+ patch.runTimeoutMs = 0;
101568
+ } else {
101569
+ const minutes = wholeMinutes(value);
101570
+ if (minutes === undefined || minutes < 1 || minutes > MAX_LIMIT_MINUTES) {
101571
+ return { action: "invalid", message: `limit must be none or a whole number of minutes from 1 to ${MAX_LIMIT_MINUTES}.` };
101572
+ }
101573
+ patch.runTimeoutMs = minutes * 60000;
101574
+ }
101575
+ } else if (key === "wait") {
101576
+ if (patch.approvalTimeoutMs !== undefined)
101577
+ return { action: "invalid", message: REMOTE_POLICY_USAGE };
101578
+ const minutes = wholeMinutes(value);
101579
+ if (minutes === undefined || minutes < 1 || minutes > MAX_WAIT_MINUTES) {
101580
+ return { action: "invalid", message: `wait must be a whole number of minutes from 1 to ${MAX_WAIT_MINUTES}.` };
101581
+ }
101582
+ patch.approvalTimeoutMs = minutes * 60000;
101583
+ } else {
101584
+ return { action: "invalid", message: REMOTE_POLICY_USAGE };
101585
+ }
101586
+ index += 2;
101587
+ }
101588
+ return { action: "set", patch };
101589
+ }
101590
+ function loadPosture(deps = {}) {
101591
+ const base = deps.running?.() ?? (() => {
101592
+ const loaded = loadRemoteConfig(deps.dir);
101593
+ return loaded.ok ? { defaultMode: loaded.value.permissionMode, runTimeoutMs: loaded.value.runTimeoutMs, approvalTimeoutMs: loaded.value.approvalTimeoutMs } : undefined;
101594
+ })();
101595
+ if (base === undefined)
101596
+ return;
101597
+ const inForce = deps.inForce?.();
101598
+ return { ...base, ...inForce !== undefined ? { inForce } : {}, savedRules: countSavedRules(deps.dir) };
101599
+ }
101600
+ function remotePolicyText(rest, deps = {}) {
101601
+ const request = parseRemotePolicyArgs(rest);
101602
+ if (request.action === "invalid")
101603
+ return `${request.message}
101604
+ `;
101605
+ if (request.action === "set") {
101606
+ const saved = updateRemotePolicy(request.patch, deps.dir);
101607
+ if (!saved.ok)
101608
+ return `Not changed: ${saved.reason}
101609
+ `;
101610
+ deps.applyPolicy?.(request.patch);
101611
+ const posture = loadPosture(deps);
101612
+ const lines = [
101613
+ "Saved. Telegram defaults:",
101614
+ ...posture !== undefined ? postureLines(posture) : [],
101615
+ deps.applyPolicy !== undefined ? "This shell uses them from the next Telegram turn; a shell that registers later gets them when keryx serve restarts." : "Open shells keep their values until they are restarted; keryx serve delivers these to a shell that registers after it restarts.",
101616
+ SHELL_MODE_NOTE
101617
+ ];
101618
+ return `${lines.join(`
101619
+ `)}
101620
+ `;
101621
+ }
101622
+ const posture = loadPosture(deps);
101623
+ if (posture === undefined) {
101624
+ return `Telegram is not connected, so there is nothing to change yet. Connect it with /channels.
101625
+ ${REMOTE_POLICY_USAGE}
101626
+ `;
101627
+ }
101628
+ return `${["Telegram permissions:", ...postureLines(posture), SHELL_MODE_NOTE, REMOTE_POLICY_USAGE].join(`
101629
+ `)}
101630
+ `;
101631
+ }
101632
+ var REMOTE_POLICY_COMMAND = "/remote-policy", REMOTE_POLICY_USAGE, MAX_LIMIT_MINUTES, MAX_WAIT_MINUTES = 60, SHELL_MODE_NOTE = "A /mode in the shell (or typed in the topic) changes the shell's mode and wins for every turn after that; /remote-policy never changes it.";
101633
+ var init_remote_policy_command = __esm(() => {
101634
+ init_config11();
101635
+ init_permissions_command();
101636
+ REMOTE_POLICY_USAGE = `Usage: ${REMOTE_POLICY_COMMAND} [mode ask|trust] [limit none|<minutes>] [wait <minutes>]`;
101637
+ MAX_LIMIT_MINUTES = 7 * 24 * 60;
101638
+ });
101639
+
101222
101640
  // src/tui/reasoning-display.ts
101223
101641
  function parseThinkDisplayMode(raw) {
101224
101642
  if (raw === undefined) {
@@ -101798,6 +102216,63 @@ function reasoningScope(reasoning) {
101798
102216
  return { scope: "saved" };
101799
102217
  }
101800
102218
  }
102219
+ function telegramRows(state) {
102220
+ if (state.telegram === undefined)
102221
+ return [];
102222
+ const posture = state.telegram.posture;
102223
+ const notConnected = "not connected (/channels)";
102224
+ const modeDetail = posture === undefined ? "connect Telegram first" : `${posture.inForce !== undefined ? `in force: ${posture.inForce}; ` : ""}a /mode in the shell wins once used`;
102225
+ const minutes = (ms) => Math.round(ms / 60000);
102226
+ return [
102227
+ {
102228
+ id: "tg-mode",
102229
+ group: "Telegram",
102230
+ label: "Telegram mode",
102231
+ value: posture?.defaultMode ?? notConnected,
102232
+ scope: "saved",
102233
+ detail: modeDetail,
102234
+ command: "/remote-policy",
102235
+ usage: "/remote-policy mode ask|trust",
102236
+ actions: posture === undefined ? [] : ["ask", "trust"].map((mode) => action(mode, `/remote-policy mode ${mode}`, mode === posture.defaultMode))
102237
+ },
102238
+ {
102239
+ id: "tg-limit",
102240
+ group: "Telegram",
102241
+ label: "Telegram run limit",
102242
+ value: posture === undefined ? notConnected : formatPolicyDuration(posture.runTimeoutMs),
102243
+ scope: "saved",
102244
+ detail: "/stop in the topic always ends a run",
102245
+ command: "/remote-policy",
102246
+ usage: "/remote-policy limit none|<minutes>",
102247
+ actions: posture === undefined ? [] : [
102248
+ action("None", "/remote-policy limit none", posture.runTimeoutMs === 0),
102249
+ ...LIMIT_CHOICES_MIN.map((m) => action(`${m} min`, `/remote-policy limit ${m}`, minutes(posture.runTimeoutMs) === m && posture.runTimeoutMs > 0))
102250
+ ]
102251
+ },
102252
+ {
102253
+ id: "tg-wait",
102254
+ group: "Telegram",
102255
+ label: "Telegram approval wait",
102256
+ value: posture === undefined ? notConnected : formatPolicyDuration(posture.approvalTimeoutMs),
102257
+ scope: "saved",
102258
+ detail: "a prompt nobody answers in this time is denied",
102259
+ command: "/remote-policy",
102260
+ usage: "/remote-policy wait <minutes>",
102261
+ actions: posture === undefined ? [] : WAIT_CHOICES_MIN.map((m) => action(`${m} min`, `/remote-policy wait ${m}`, posture.approvalTimeoutMs === m * 60000))
102262
+ },
102263
+ {
102264
+ id: "tg-rules",
102265
+ group: "Telegram",
102266
+ label: "Saved shell rules",
102267
+ value: posture === undefined ? "none" : String(posture.savedRules),
102268
+ scope: "saved",
102269
+ detail: "what Always remembered, from the shell or Telegram",
102270
+ command: "/permissions",
102271
+ usage: "/permissions [list | remove <number | pattern>]",
102272
+ actions: []
102273
+ }
102274
+ ];
102275
+ }
101801
102276
  function buildSettingsRows(state) {
101802
102277
  const reasoning = reasoningScope(state.reasoning);
101803
102278
  return [
@@ -101931,7 +102406,8 @@ function buildSettingsRows(state) {
101931
102406
  command: "/jevprofile",
101932
102407
  usage: "/jevprofile",
101933
102408
  actions: []
101934
- }
102409
+ },
102410
+ ...telegramRows(state)
101935
102411
  ];
101936
102412
  }
101937
102413
  function formatSettingsTable(rows, options = {}) {
@@ -101955,19 +102431,22 @@ function formatSettingsTable(rows, options = {}) {
101955
102431
  `)}
101956
102432
  `;
101957
102433
  }
101958
- var SETTING_GROUPS, onOff = (value) => value ? "on" : "off", EXTERNAL_PRIVACY_DETAIL;
102434
+ var SETTING_GROUPS, onOff = (value) => value ? "on" : "off", EXTERNAL_PRIVACY_DETAIL, LIMIT_CHOICES_MIN, WAIT_CHOICES_MIN;
101959
102435
  var init_settings_model = __esm(() => {
101960
102436
  init_agent();
101961
102437
  init_permission_mode();
101962
102438
  init_rendering_mode();
102439
+ init_remote_policy_command();
101963
102440
  init_reasoning_display();
101964
102441
  init_theme();
101965
- SETTING_GROUPS = ["Safety", "Routing", "Display", "External"];
102442
+ SETTING_GROUPS = ["Safety", "Routing", "Display", "External", "Telegram"];
101966
102443
  EXTERNAL_PRIVACY_DETAIL = {
101967
102444
  project: "this project overrides the per-user setting",
101968
102445
  user: undefined,
101969
102446
  default: "default"
101970
102447
  };
102448
+ LIMIT_CHOICES_MIN = [30, 60];
102449
+ WAIT_CHOICES_MIN = [5, 15, 30];
101971
102450
  });
101972
102451
 
101973
102452
  // src/review/jev-profile.ts
@@ -102630,6 +103109,25 @@ async function orElse(read, fallback) {
102630
103109
  return fallback;
102631
103110
  }
102632
103111
  }
103112
+ function readTelegram(live, configDir) {
103113
+ try {
103114
+ const running = live.telegram;
103115
+ const posture = loadPosture({
103116
+ ...configDir !== undefined ? { dir: configDir } : {},
103117
+ ...running !== undefined ? {
103118
+ running: () => ({
103119
+ defaultMode: running.defaultMode,
103120
+ runTimeoutMs: running.runTimeoutMs,
103121
+ approvalTimeoutMs: running.approvalTimeoutMs
103122
+ }),
103123
+ ...running.inForce !== undefined ? { inForce: () => running.inForce } : {}
103124
+ } : {}
103125
+ });
103126
+ return posture === undefined ? {} : { posture };
103127
+ } catch {
103128
+ return {};
103129
+ }
103130
+ }
102633
103131
  async function loadSettingsSnapshot(cwd, live, configDir) {
102634
103132
  const saved = loadShellConfig(configDir);
102635
103133
  const projectMode = getProjectPermissionMode(cwd, configDir);
@@ -102655,12 +103153,14 @@ async function loadSettingsSnapshot(cwd, live, configDir) {
102655
103153
  total: RECOMMENDED_JEV_PROFILE.length
102656
103154
  },
102657
103155
  externalAgents: agents.ok ? { on: true } : { on: false, reason: agents.reason },
102658
- rendering: readRendering(configDir)
103156
+ rendering: readRendering(configDir),
103157
+ telegram: readTelegram(live, configDir)
102659
103158
  };
102660
103159
  }
102661
103160
  var init_settings_state = __esm(() => {
102662
103161
  init_external_agents();
102663
103162
  init_agent();
103163
+ init_remote_policy_command();
102664
103164
  init_review_jev_profile();
102665
103165
  init_external_switch();
102666
103166
  init_permission_mode_config();
@@ -104124,6 +104624,12 @@ var init_help_groups = __esm(() => {
104124
104624
  group: "Automation",
104125
104625
  summary: "Pending remote approvals: list them, allow or deny one call, once (the local answer path of keryx serve)."
104126
104626
  },
104627
+ {
104628
+ kind: "cli",
104629
+ name: "permissions",
104630
+ group: "Automation",
104631
+ summary: "The saved shell rules an Always answer leaves behind: list them (honoured or not, with the reason) and remove one."
104632
+ },
104127
104633
  {
104128
104634
  kind: "cli",
104129
104635
  name: "governance",
@@ -104178,6 +104684,12 @@ var init_help_groups = __esm(() => {
104178
104684
  group: "Automation",
104179
104685
  summary: "Pending remote approvals with scope, consequence and expiry \u2014 allow or deny one call, once."
104180
104686
  },
104687
+ {
104688
+ kind: "slash",
104689
+ name: "/permissions",
104690
+ group: "Automation",
104691
+ summary: "The saved shell rules (what Always remembered): honoured or not, session grants \u2014 remove one."
104692
+ },
104181
104693
  {
104182
104694
  kind: "slash",
104183
104695
  name: "/external-diff",
@@ -104226,6 +104738,12 @@ var init_help_groups = __esm(() => {
104226
104738
  group: "Automation",
104227
104739
  summary: "How Telegram replies are written: tables and lists as rich messages or HTML, with a fallback. [auto|rich|html|plain]."
104228
104740
  },
104741
+ {
104742
+ kind: "slash",
104743
+ name: "/remote-policy",
104744
+ group: "Automation",
104745
+ summary: "Telegram defaults for a turn started there: mode ask|trust, run limit, approval wait. Never changes the shell's /mode."
104746
+ },
104229
104747
  {
104230
104748
  kind: "cli",
104231
104749
  name: "acp",
@@ -114453,6 +114971,155 @@ var init_reviews_inspector = __esm(() => {
114453
114971
  ];
114454
114972
  });
114455
114973
 
114974
+ // src/tui/permissions-inspector.ts
114975
+ function formatPermissionsModal(view, selected, notice) {
114976
+ const lines = [];
114977
+ const rowStart = [];
114978
+ const active = view.rows.filter((row) => row.kind === "active").length;
114979
+ lines.push(view.rows.length === 0 ? "No saved shell rules: every command that is not read-only asks first." : `Saved shell rules (${active}) - d removes the selected one, it asks again from then on`);
114980
+ if (notice !== undefined && notice.length > 0)
114981
+ lines.push(notice);
114982
+ lines.push("");
114983
+ let section;
114984
+ view.rows.forEach((row, index) => {
114985
+ if (row.kind !== section) {
114986
+ section = row.kind;
114987
+ if (row.kind === "inactive")
114988
+ lines.push("Not honoured: kept in the file, they approve nothing", "");
114989
+ if (row.kind === "session")
114990
+ lines.push("Granted for this session only", "");
114991
+ }
114992
+ rowStart.push(lines.length);
114993
+ lines.push(`${index === selected ? ">" : " "} ${row.n}. ${patternOnOneLine(row.pattern)}`);
114994
+ if (row.reason !== undefined)
114995
+ lines.push(` ${row.reason}`);
114996
+ lines.push("");
114997
+ });
114998
+ lines.push(`File: ${view.path}`);
114999
+ return { rows: view.rows, lines, rowStart };
115000
+ }
115001
+ function openPermissions(otui, chrome, options) {
115002
+ const core = otui;
115003
+ const r = chrome?.renderer;
115004
+ const rendererHint = options.renderer ?? chrome?.renderer;
115005
+ const panelRows = typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13;
115006
+ const bodyRows = Math.max(1, options.visibleRows ?? panelRows);
115007
+ const load = options.load ?? (() => loadPermissionsView(options.sessionAllow !== undefined ? { sessionAllow: options.sessionAllow } : {}));
115008
+ const remove = options.remove ?? ((target) => removePermission(target, {
115009
+ ...options.sessionAllow !== undefined ? { sessionAllow: options.sessionAllow } : {},
115010
+ ...options.onChanged !== undefined ? { onChanged: options.onChanged } : {}
115011
+ }));
115012
+ let selected = 0;
115013
+ let scroll = 0;
115014
+ let width;
115015
+ let bodyNode;
115016
+ let closed = false;
115017
+ let notice;
115018
+ let armed;
115019
+ const keys = {};
115020
+ const model = () => formatPermissionsModal(load(), selected, armed !== undefined ? `Remove ${patternOnOneLine(armed)}? press y to confirm, any other key cancels` : notice);
115021
+ const clampSelected = (rows) => {
115022
+ selected = Math.min(Math.max(0, rows - 1), Math.max(0, selected));
115023
+ };
115024
+ const visible = () => {
115025
+ const view = model();
115026
+ clampSelected(view.rows.length);
115027
+ return windowLines(wrapLines(view.lines.join(`
115028
+ `), width).split(`
115029
+ `), scroll, bodyRows);
115030
+ };
115031
+ const paint = () => {
115032
+ if (closed)
115033
+ return;
115034
+ const view = model();
115035
+ clampSelected(view.rows.length);
115036
+ const wrapped = wrapLines(view.lines.join(`
115037
+ `), width).split(`
115038
+ `);
115039
+ const start = view.rowStart[selected] ?? 0;
115040
+ scroll = clampScroll(scrollToReveal(start, scroll, bodyRows), wrapped.length, bodyRows);
115041
+ if (bodyNode !== undefined)
115042
+ bodyNode.content = core.t`${dimChunk(core, windowLines(wrapped, scroll, bodyRows).join(`
115043
+ `))}`;
115044
+ };
115045
+ let unsubscribeTheme = () => {};
115046
+ const handle = openModal(core, chrome, {
115047
+ title: PERMISSIONS_COMMAND,
115048
+ tabs: [{ id: "rules", label: "Shell rules" }],
115049
+ footer: PERMISSIONS_FOOTER,
115050
+ renderTab: (_tabId, body, ctx) => {
115051
+ width = ctx.width;
115052
+ const parent = body;
115053
+ bodyNode = new core.TextRenderable(r, { id: "permissions-body", content: "" });
115054
+ parent.add(bodyNode);
115055
+ paint();
115056
+ },
115057
+ onClose: () => {
115058
+ closed = true;
115059
+ keys.off?.();
115060
+ unsubscribeTheme();
115061
+ }
115062
+ });
115063
+ if (handle === undefined)
115064
+ return;
115065
+ const modal = handle;
115066
+ unsubscribeTheme = onThemeChange(guardedThemeRepaint("permissions-modal", paint, () => closed || isRenderableGone(bodyNode) || r?.isDestroyed === true));
115067
+ const selectedRow = () => model().rows[selected];
115068
+ keys.off = options.onKeypress((key) => {
115069
+ if (closed || options.inputBlocked?.() === true)
115070
+ return;
115071
+ const token = key.name || key.sequence;
115072
+ if (armed !== undefined) {
115073
+ const pattern = armed;
115074
+ armed = undefined;
115075
+ if (token === "y") {
115076
+ notice = remove(pattern).text;
115077
+ }
115078
+ paint();
115079
+ return;
115080
+ }
115081
+ if (token === "up" || token === "k") {
115082
+ selected = Math.max(0, selected - 1);
115083
+ notice = undefined;
115084
+ } else if (token === "down" || token === "j") {
115085
+ selected += 1;
115086
+ notice = undefined;
115087
+ } else if (token === "d") {
115088
+ const row = selectedRow();
115089
+ if (row === undefined)
115090
+ notice = "Nothing to remove.";
115091
+ else
115092
+ armed = row.pattern;
115093
+ } else {
115094
+ return;
115095
+ }
115096
+ paint();
115097
+ });
115098
+ return {
115099
+ ...modal,
115100
+ visibleLines: visible,
115101
+ selected: selectedRow,
115102
+ reload: () => {
115103
+ paint();
115104
+ }
115105
+ };
115106
+ }
115107
+ var PERMISSIONS_COMMAND = "/permissions", PERMISSIONS_FOOTER;
115108
+ var init_permissions_inspector = __esm(() => {
115109
+ init_permissions_command();
115110
+ init_flow_inspector();
115111
+ init_modal_host();
115112
+ init_theme();
115113
+ init_theme_repaint();
115114
+ init_theme_text();
115115
+ PERMISSIONS_FOOTER = [
115116
+ { key: "\u2191/\u2193", label: "move" },
115117
+ { key: "d", label: "remove" },
115118
+ { key: "y", label: "confirm" },
115119
+ { key: "esc", label: "close" }
115120
+ ];
115121
+ });
115122
+
114456
115123
  // src/lib/serve-config.ts
114457
115124
  import { existsSync as existsSync42 } from "fs";
114458
115125
  import path249 from "path";
@@ -114864,7 +115531,7 @@ class SseParser {
114864
115531
  return frames;
114865
115532
  }
114866
115533
  }
114867
- var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_APPROVAL_ACK_MS = 5000, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
115534
+ var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_APPROVAL_ACK_MS = 5000, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MAX_REMEMBER_PATTERN_CHARS = 300, APPROVAL_INPUT_PREVIEW_CHARS = 1500, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
114868
115535
 
114869
115536
  `;
114870
115537
  var init_protocol2 = __esm(() => {
@@ -114874,6 +115541,7 @@ var init_protocol2 = __esm(() => {
114874
115541
  heartbeat: "POST",
114875
115542
  reply: "POST",
114876
115543
  approval: "POST",
115544
+ "approval-result": "POST",
114877
115545
  prompt: "POST",
114878
115546
  state: "POST",
114879
115547
  ack: "POST",
@@ -114894,7 +115562,7 @@ var init_protocol2 = __esm(() => {
114894
115562
  MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
114895
115563
  SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
114896
115564
  APPROVAL_ID_PATTERN = /^ap[0-9a-f]{12}$/;
114897
- APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny)$/;
115565
+ APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny|always)$/;
114898
115566
  CHOICE_CALLBACK_PATTERN = /^pk:(pk[0-9a-f]{12}):(\d{1,3})$/;
114899
115567
  MESSAGE_STATES = ["reading", "working", "done", "failed"];
114900
115568
  });
@@ -115202,6 +115870,16 @@ var init_shell_token = __esm(() => {
115202
115870
  });
115203
115871
 
115204
115872
  // src/remote/client.ts
115873
+ function readApprovalEvent(parsed) {
115874
+ if (parsed === null || typeof parsed !== "object")
115875
+ return;
115876
+ const o = parsed;
115877
+ if (typeof o.approvalId !== "string" || o.approvalId.length === 0)
115878
+ return;
115879
+ const decision = o.decision === "allow" || o.decision === "always" ? o.decision : "deny";
115880
+ const fromId = typeof o.fromId === "number" && Number.isSafeInteger(o.fromId) ? o.fromId : undefined;
115881
+ return { approvalId: o.approvalId, pressed: { decision, ...fromId === undefined ? {} : { fromId } } };
115882
+ }
115205
115883
  function ownProcessIsAlive(pid) {
115206
115884
  if (!Number.isInteger(pid) || pid <= 0) {
115207
115885
  return false;
@@ -115268,6 +115946,8 @@ class RemoteClient {
115268
115946
  name;
115269
115947
  threadId;
115270
115948
  runTimeoutMs;
115949
+ permissionMode;
115950
+ approvalTimeoutMs;
115271
115951
  lastHeartbeatAt;
115272
115952
  constructor(options) {
115273
115953
  this.options = options;
@@ -115363,38 +116043,71 @@ class RemoteClient {
115363
116043
  }
115364
116044
  }
115365
116045
  async requestApproval(prompt, timeoutMs) {
115366
- if (!this.connected) {
115367
- return "deny";
116046
+ return (await this.askApproval(prompt, timeoutMs)).decision;
116047
+ }
116048
+ async askApproval(prompt, timeoutMs, options = {}) {
116049
+ if (!this.connected || options.signal?.aborted === true) {
116050
+ return { decision: "deny" };
115368
116051
  }
115369
- const body = { sessionId: this.options.sessionId, prompt, timeoutMs };
116052
+ const body = {
116053
+ sessionId: this.options.sessionId,
116054
+ prompt,
116055
+ timeoutMs,
116056
+ ...options.remember === undefined ? {} : { remember: options.remember }
116057
+ };
115370
116058
  let approvalId;
115371
116059
  try {
115372
116060
  const response = await this.post("approval", body);
115373
116061
  if (!response.ok) {
115374
- return "deny";
116062
+ return { decision: "deny" };
116063
+ }
116064
+ const id = (await response.json())?.approvalId;
116065
+ if (typeof id !== "string" || id.length === 0) {
116066
+ return { decision: "deny" };
115375
116067
  }
115376
- approvalId = (await response.json()).approvalId;
116068
+ approvalId = id;
115377
116069
  } catch {
115378
- return "deny";
116070
+ return { decision: "deny" };
115379
116071
  }
115380
116072
  const early = this.earlyDecisions.get(approvalId);
115381
116073
  if (early !== undefined) {
115382
116074
  clearTimeout(early.timer);
115383
116075
  this.earlyDecisions.delete(approvalId);
115384
- this.settleApproval(approvalId, early.decision, true);
115385
- return early.decision;
116076
+ this.settleApproval(approvalId, early.pressed.decision, true);
116077
+ return { ...early.pressed, approvalId };
115386
116078
  }
115387
116079
  if (!this.connected) {
115388
- return "deny";
116080
+ return { decision: "deny" };
115389
116081
  }
115390
116082
  return new Promise((resolve) => {
116083
+ const signal = options.signal;
116084
+ const onAbort = () => {
116085
+ const waiter = this.waiters.get(approvalId);
116086
+ if (waiter !== undefined)
116087
+ clearTimeout(waiter.timer);
116088
+ this.waiters.delete(approvalId);
116089
+ resolve({ decision: "deny", approvalId });
116090
+ };
115391
116091
  const timer = setTimeout(() => {
116092
+ signal?.removeEventListener("abort", onAbort);
115392
116093
  this.waiters.delete(approvalId);
115393
- resolve("deny");
116094
+ resolve({ decision: "deny" });
115394
116095
  }, timeoutMs);
115395
- this.waiters.set(approvalId, { resolve, timer });
116096
+ this.waiters.set(approvalId, {
116097
+ resolve: (pressed) => {
116098
+ signal?.removeEventListener("abort", onAbort);
116099
+ resolve({ ...pressed, approvalId });
116100
+ },
116101
+ timer
116102
+ });
116103
+ signal?.addEventListener("abort", onAbort, { once: true });
115396
116104
  });
115397
116105
  }
116106
+ async reportApprovalResult(approvalId, remembered) {
116107
+ try {
116108
+ await this.post("approval-result", { sessionId: this.options.sessionId, approvalId, remembered });
116109
+ } catch {}
116110
+ }
115398
116111
  async requestChoice(text, rows, timeoutMs, forUserId) {
115399
116112
  if (!this.connected) {
115400
116113
  return;
@@ -115565,7 +116278,14 @@ class RemoteClient {
115565
116278
  this.lastHeartbeatAt = Date.now();
115566
116279
  this.options.onStatus?.({ state: "connected" });
115567
116280
  if (this.name !== undefined && this.threadId !== undefined && this.runTimeoutMs !== undefined) {
115568
- this.settleFirst({ ok: true, name: this.name, threadId: this.threadId, runTimeoutMs: this.runTimeoutMs });
116281
+ this.settleFirst({
116282
+ ok: true,
116283
+ name: this.name,
116284
+ threadId: this.threadId,
116285
+ runTimeoutMs: this.runTimeoutMs,
116286
+ ...this.permissionMode === undefined ? {} : { permissionMode: this.permissionMode },
116287
+ ...this.approvalTimeoutMs === undefined ? {} : { approvalTimeoutMs: this.approvalTimeoutMs }
116288
+ });
115569
116289
  }
115570
116290
  } else if (status.kind === "superseded") {
115571
116291
  throw new FatalClientError("superseded", "another connection for this session replaced this one");
@@ -115601,6 +116321,8 @@ class RemoteClient {
115601
116321
  this.name = result.name;
115602
116322
  this.threadId = result.threadId;
115603
116323
  this.runTimeoutMs = result.runTimeoutMs;
116324
+ this.permissionMode = result.permissionMode;
116325
+ this.approvalTimeoutMs = result.approvalTimeoutMs;
115604
116326
  return;
115605
116327
  }
115606
116328
  const error = await response.json().catch(() => ({}));
@@ -115669,12 +116391,12 @@ class RemoteClient {
115669
116391
  await this.post("ack", { sessionId: this.options.sessionId, updateId });
115670
116392
  } catch {}
115671
116393
  }
115672
- handleApprovalFrame(approval) {
115673
- const approvalId = approval?.approvalId;
115674
- if (!isApprovalId(approvalId) || approval.decision !== "allow" && approval.decision !== "deny") {
116394
+ handleApprovalFrame(parsed) {
116395
+ const read = readApprovalEvent(parsed);
116396
+ if (read === undefined || !isApprovalId(read.approvalId)) {
115675
116397
  return;
115676
116398
  }
115677
- const decision = approval.decision;
116399
+ const { approvalId, pressed } = read;
115678
116400
  const seen = this.seenApprovals.get(approvalId);
115679
116401
  if (seen === "pending") {
115680
116402
  return;
@@ -115685,11 +116407,11 @@ class RemoteClient {
115685
116407
  return;
115686
116408
  }
115687
116409
  if (this.waiters.has(approvalId)) {
115688
- this.settleApproval(approvalId, decision, true, () => this.resolveApproval(approvalId, decision));
116410
+ this.settleApproval(approvalId, pressed.decision, true, () => this.resolveApproval(approvalId, pressed));
115689
116411
  return;
115690
116412
  }
115691
116413
  this.rememberSeen(approvalId, "pending");
115692
- this.parkEarlyDecision(approvalId, decision);
116414
+ this.parkEarlyDecision(approvalId, pressed);
115693
116415
  }
115694
116416
  rememberSeen(approvalId, outcome) {
115695
116417
  this.seenApprovals.delete(approvalId);
@@ -115721,7 +116443,7 @@ class RemoteClient {
115721
116443
  apply?.();
115722
116444
  this.ackApproval(approvalId, applied);
115723
116445
  }
115724
- parkEarlyDecision(approvalId, decision) {
116446
+ parkEarlyDecision(approvalId, pressed) {
115725
116447
  while (this.earlyDecisions.size >= MAX_EARLY_DECISIONS) {
115726
116448
  const oldest = this.earlyDecisions.keys().next().value;
115727
116449
  if (oldest === undefined) {
@@ -115731,7 +116453,7 @@ class RemoteClient {
115731
116453
  }
115732
116454
  const timer = setTimeout(() => this.dropEarlyDecision(approvalId), this.earlyDecisionMs);
115733
116455
  timer.unref?.();
115734
- this.earlyDecisions.set(approvalId, { decision, timer });
116456
+ this.earlyDecisions.set(approvalId, { pressed, timer });
115735
116457
  }
115736
116458
  dropEarlyDecision(approvalId) {
115737
116459
  const early = this.earlyDecisions.get(approvalId);
@@ -115741,7 +116463,7 @@ class RemoteClient {
115741
116463
  clearTimeout(early.timer);
115742
116464
  this.earlyDecisions.delete(approvalId);
115743
116465
  if (!this.stopped) {
115744
- this.settleApproval(approvalId, early.decision, false);
116466
+ this.settleApproval(approvalId, early.pressed.decision, false);
115745
116467
  }
115746
116468
  }
115747
116469
  async ackApproval(approvalId, applied) {
@@ -115812,7 +116534,7 @@ class RemoteClient {
115812
116534
  for (const [id, waiter] of [...this.waiters.entries()]) {
115813
116535
  clearTimeout(waiter.timer);
115814
116536
  this.waiters.delete(id);
115815
- waiter.resolve("deny");
116537
+ waiter.resolve({ decision: "deny" });
115816
116538
  }
115817
116539
  for (const id of [...this.earlyDecisions.keys()]) {
115818
116540
  this.dropEarlyDecision(id);
@@ -115976,6 +116698,7 @@ var init_command_gateway = __esm(() => {
115976
116698
  { name: "connect", kind: "picker", description: "Switch to a connected provider with buttons" },
115977
116699
  { name: "resume", kind: "picker", description: "Return to an earlier session with buttons" },
115978
116700
  { name: "sessions", kind: "builtin", description: "List recent sessions" },
116701
+ { name: "stop", kind: "builtin", description: "Stop the run you started from Telegram" },
115979
116702
  { name: "new", kind: "text", description: "Start a new session; this topic stays bound to it" },
115980
116703
  { name: "clear", kind: "text", description: "Same as /new" },
115981
116704
  { name: "compact", kind: "text", description: "Compact the conversation: /compact [focus]" },
@@ -116021,7 +116744,9 @@ var init_command_gateway = __esm(() => {
116021
116744
  governance: "it needs the shell's screen",
116022
116745
  decisions: "it opens a panel in the shell",
116023
116746
  expand: "it needs the shell's screen",
116024
- interrupt: "use the Stop button on a message, or stop it in the shell",
116747
+ interrupt: "use /stop here to end a run you started from Telegram, or stop it in the shell",
116748
+ permissions: "it opens a panel in the shell; saved rules are changed there, never from a chat",
116749
+ "remote-policy": "it changes what Telegram may do without asking; use the shell",
116025
116750
  demote: "it works on the shell's own tasks",
116026
116751
  models: "use /model",
116027
116752
  "external-diff": "listing and applying external patches stays in the shell, where the patch hash is typed",
@@ -116072,6 +116797,8 @@ class RemoteCommandRouter {
116072
116797
  await this.listSessionsText();
116073
116798
  ended(end, "done");
116074
116799
  });
116800
+ } else if (decision.command === "stop") {
116801
+ this.stop(end);
116075
116802
  } else {
116076
116803
  this.ctx.reply(this.ctx.compose(remoteHelpText())).catch(() => false);
116077
116804
  ended(end, "done");
@@ -116098,6 +116825,17 @@ class RemoteCommandRouter {
116098
116825
  return;
116099
116826
  }
116100
116827
  }
116828
+ stop(end) {
116829
+ const result = this.ctx.host.stopTelegramTurn?.() ?? "idle";
116830
+ if (result === "stopped") {
116831
+ ended(end, "done");
116832
+ return;
116833
+ }
116834
+ const text = result === "operator" ? "/stop did nothing: the running turn was started in the shell, so only the shell can stop it." : "/stop did nothing: no run started from Telegram is going on.";
116835
+ this.ctx.record(`/stop: ${result === "operator" ? "turn belongs to the shell" : "nothing to stop"}`);
116836
+ this.ctx.reply(text).catch(() => false);
116837
+ ended(end, "failed");
116838
+ }
116101
116839
  async idle() {
116102
116840
  await this.chain;
116103
116841
  if (this.detached > 0) {
@@ -116384,9 +117122,27 @@ function composeReply(text) {
116384
117122
  return `${safe.slice(0, REMOTE_REPLY_MAX_CHARS)}
116385
117123
  [cut: the full reply is in the shell]`;
116386
117124
  }
117125
+ function mapApprovalAnswer(got, offered) {
117126
+ if (got === null || typeof got !== "object")
117127
+ return { decision: "deny", always: false };
117128
+ const o = got;
117129
+ const yes = o.decision === "allow" || o.decision === "always";
117130
+ const approvalId = typeof o.approvalId === "string" && o.approvalId.length > 0 ? o.approvalId : undefined;
117131
+ const fromId = typeof o.fromId === "number" && Number.isSafeInteger(o.fromId) ? o.fromId : undefined;
117132
+ return {
117133
+ decision: yes ? "allow" : "deny",
117134
+ always: o.decision === "always" && offered,
117135
+ ...approvalId === undefined ? {} : { approvalId },
117136
+ ...fromId === undefined ? {} : { fromId }
117137
+ };
117138
+ }
116387
117139
  function composeApprovalPrompt(prompt) {
116388
117140
  const safe = redactSensitiveText(prompt).trim();
116389
- return safe.length <= MAX_APPROVAL_PROMPT_CHARS ? safe : `${safe.slice(0, MAX_APPROVAL_PROMPT_CHARS - 1)}\u2026`;
117141
+ if (safe.length <= MAX_APPROVAL_PROMPT_CHARS)
117142
+ return safe;
117143
+ const note = `
117144
+ [cut: the rest of this prompt is in the shell]`;
117145
+ return `${safe.slice(0, MAX_APPROVAL_PROMPT_CHARS - note.length - 1)}\u2026${note}`;
116390
117146
  }
116391
117147
  function describeApprovalForTopic(tool, inputJson, meta) {
116392
117148
  const lines = [];
@@ -116406,8 +117162,12 @@ function describeApprovalForTopic(tool, inputJson, meta) {
116406
117162
  shown = o.patch;
116407
117163
  }
116408
117164
  } catch {}
116409
- shown = shown.length > APPROVAL_INPUT_PREVIEW_CHARS ? `${shown.slice(0, APPROVAL_INPUT_PREVIEW_CHARS)}\u2026` : shown;
117165
+ const total = shown.length;
117166
+ shown = total > APPROVAL_INPUT_PREVIEW_CHARS ? `${shown.slice(0, APPROVAL_INPUT_PREVIEW_CHARS)}\u2026` : shown;
116410
117167
  lines.push(`Approve ${tool}?`, shown);
117168
+ if (total > APPROVAL_INPUT_PREVIEW_CHARS) {
117169
+ lines.push(`[cut: only the first ${APPROVAL_INPUT_PREVIEW_CHARS} of ${total} characters are shown; the rest is in the shell]`);
117170
+ }
116411
117171
  }
116412
117172
  if (meta?.destructive === true)
116413
117173
  lines.push("Warning: this can delete or overwrite files.");
@@ -116425,28 +117185,33 @@ class RemoteBridge {
116425
117185
  events = [];
116426
117186
  host;
116427
117187
  now;
116428
- approvalTimeoutMs;
117188
+ approvalOverrideMs;
117189
+ policyOverride = {};
116429
117190
  connectedOnce = false;
116430
117191
  clientConnected = false;
116431
117192
  tgTurn = false;
116432
117193
  lastAssistantText = "";
116433
117194
  runTimer;
116434
117195
  timedOut = false;
117196
+ stoppedByUser = false;
117197
+ turnAbort;
116435
117198
  closing;
116436
117199
  router;
116437
117200
  keepTopic = false;
116438
117201
  switched = false;
116439
117202
  waiting = [];
116440
117203
  currentUpdate;
117204
+ currentFromId;
116441
117205
  constructor(options) {
116442
117206
  this.options = options;
116443
117207
  this.host = options.host;
116444
117208
  this.now = options.now ?? Date.now;
116445
- this.approvalTimeoutMs = options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
117209
+ this.approvalOverrideMs = options.approvalTimeoutMs;
116446
117210
  this.router = new RemoteCommandRouter({
116447
117211
  host: {
116448
117212
  isBusy: () => this.host.isBusy(),
116449
117213
  cancelTurn: () => this.host.cancelTurn(),
117214
+ stopTelegramTurn: () => this.stopTelegramTurn(),
116450
117215
  ...this.host.runCommand !== undefined ? { runCommand: (line) => this.host.runCommand(line) } : {},
116451
117216
  ...this.host.busyRefusal !== undefined ? { busyRefusal: (line) => this.host.busyRefusal(line) } : {},
116452
117217
  ...this.host.listModels !== undefined ? { listModels: (id) => this.host.listModels(id) } : {},
@@ -116493,6 +117258,25 @@ class RemoteBridge {
116493
117258
  get telegramTurnActive() {
116494
117259
  return this.tgTurn;
116495
117260
  }
117261
+ get telegramTurnUserId() {
117262
+ return this.tgTurn ? this.currentFromId : undefined;
117263
+ }
117264
+ get configuredPermissionMode() {
117265
+ return this.policyOverride.permissionMode ?? this.client?.permissionMode ?? "ask";
117266
+ }
117267
+ get approvalTimeoutMs() {
117268
+ return this.policyOverride.approvalTimeoutMs ?? this.approvalOverrideMs ?? this.client?.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
117269
+ }
117270
+ get runTimeoutMs() {
117271
+ return this.policyOverride.runTimeoutMs ?? this.client?.runTimeoutMs ?? 0;
117272
+ }
117273
+ applyPolicy(next) {
117274
+ this.policyOverride = { ...this.policyOverride, ...next };
117275
+ this.host.onChange?.();
117276
+ }
117277
+ recordApproval(text) {
117278
+ this.push("approval", preview2(text));
117279
+ }
116496
117280
  status() {
116497
117281
  const client = this.client;
116498
117282
  if (client === undefined) {
@@ -116615,7 +117399,7 @@ class RemoteBridge {
116615
117399
  return;
116616
117400
  }
116617
117401
  this.push("line", `Telegram: ${preview2(line)}`);
116618
- this.waiting.push({ updateId: meta.updateId, line });
117402
+ this.waiting.push({ updateId: meta.updateId, line, ...meta.fromId === undefined ? {} : { fromId: meta.fromId } });
116619
117403
  if (this.host.isBusy()) {
116620
117404
  this.host.enqueue(line);
116621
117405
  } else {
@@ -116670,20 +117454,37 @@ class RemoteBridge {
116670
117454
  return;
116671
117455
  const next = this.waiting.shift();
116672
117456
  this.currentUpdate = next?.updateId;
117457
+ this.currentFromId = next?.fromId;
116673
117458
  if (next !== undefined)
116674
117459
  this.report(next.updateId, "working");
116675
117460
  this.tgTurn = true;
116676
117461
  this.lastAssistantText = "";
116677
117462
  this.timedOut = false;
116678
- const limit = this.client.runTimeoutMs;
116679
- if (limit !== undefined && limit > 0) {
117463
+ this.stoppedByUser = false;
117464
+ this.turnAbort = new AbortController;
117465
+ const limit = this.runTimeoutMs;
117466
+ if (limit > 0) {
116680
117467
  this.runTimer = setTimeout(() => {
116681
117468
  this.timedOut = true;
116682
117469
  this.push("error", "run time limit reached; stopping the turn");
117470
+ this.turnAbort?.abort();
116683
117471
  this.host.cancelTurn();
116684
117472
  }, limit);
116685
117473
  }
116686
117474
  }
117475
+ stopTelegramTurn() {
117476
+ if (this.client === undefined)
117477
+ return "idle";
117478
+ if (!this.tgTurn)
117479
+ return this.host.isBusy() ? "operator" : "idle";
117480
+ if (this.stoppedByUser)
117481
+ return "stopped";
117482
+ this.stoppedByUser = true;
117483
+ this.push("command", "/stop: stopping the turn");
117484
+ this.turnAbort?.abort();
117485
+ this.host.cancelTurn();
117486
+ return "stopped";
117487
+ }
116687
117488
  assistantText(text) {
116688
117489
  if (!this.tgTurn)
116689
117490
  return;
@@ -116714,15 +117515,18 @@ class RemoteBridge {
116714
117515
  const client = this.client;
116715
117516
  const text = this.lastAssistantText;
116716
117517
  const timedOut = this.timedOut;
117518
+ const stopped = this.stoppedByUser && !timedOut;
116717
117519
  const updateId = this.currentUpdate;
116718
117520
  this.endTurnState();
116719
117521
  if (client === undefined)
116720
117522
  return;
116721
117523
  if (updateId !== undefined)
116722
- this.report(updateId, outcome.failed || timedOut ? "failed" : "done");
117524
+ this.report(updateId, outcome.failed || timedOut || stopped ? "failed" : "done");
116723
117525
  let body;
116724
- if (timedOut) {
116725
- body = "Stopped: this run went over the time limit for runs started from Telegram.";
117526
+ if (stopped) {
117527
+ body = "Stopped by you.";
117528
+ } else if (timedOut) {
117529
+ body = "Stopped: this run went over the time limit you set for runs started from Telegram (runTimeoutMs).";
116726
117530
  } else if (outcome.failed && text.trim().length === 0) {
116727
117531
  body = "The run failed. The details are in the shell.";
116728
117532
  } else if (text.trim().length === 0) {
@@ -116736,31 +117540,56 @@ class RemoteBridge {
116736
117540
  endTurnState() {
116737
117541
  this.tgTurn = false;
116738
117542
  this.currentUpdate = undefined;
117543
+ this.currentFromId = undefined;
116739
117544
  this.lastAssistantText = "";
116740
117545
  this.timedOut = false;
117546
+ this.stoppedByUser = false;
117547
+ this.turnAbort?.abort();
117548
+ this.turnAbort = undefined;
116741
117549
  if (this.runTimer !== undefined) {
116742
117550
  clearTimeout(this.runTimer);
116743
117551
  this.runTimer = undefined;
116744
117552
  }
116745
117553
  }
116746
117554
  async requestApproval(prompt) {
117555
+ return (await this.askApproval(prompt)).decision;
117556
+ }
117557
+ async askApproval(prompt, options = {}) {
116747
117558
  const client = this.client;
116748
117559
  if (client === undefined || !client.connected) {
116749
117560
  this.push("approval", "denied: not connected");
116750
- return "deny";
117561
+ return { decision: "deny", always: false };
116751
117562
  }
116752
117563
  this.push("approval", "asked in the topic");
116753
- let decision;
117564
+ let answer;
116754
117565
  try {
116755
- decision = await client.requestApproval(composeApprovalPrompt(prompt), this.approvalTimeoutMs);
117566
+ const text = composeApprovalPrompt(prompt);
117567
+ const timeoutMs = this.approvalTimeoutMs;
117568
+ if (client.askApproval !== undefined) {
117569
+ const signal = this.turnAbort?.signal;
117570
+ const got = await client.askApproval(text, timeoutMs, {
117571
+ ...options.remember === undefined ? {} : { remember: options.remember },
117572
+ ...signal === undefined ? {} : { signal }
117573
+ });
117574
+ answer = mapApprovalAnswer(got, options.remember !== undefined);
117575
+ } else {
117576
+ const decision = await client.requestApproval(text, timeoutMs);
117577
+ answer = { decision: decision === "allow" || decision === "always" ? "allow" : "deny", always: false };
117578
+ }
116756
117579
  } catch {
116757
- decision = "deny";
117580
+ answer = { decision: "deny", always: false };
116758
117581
  }
116759
- this.push("approval", decision === "allow" ? "allowed in the topic" : "denied (or no answer)");
116760
- return decision;
117582
+ const by = answer.fromId === undefined ? "" : ` by user ${answer.fromId}`;
117583
+ this.push("approval", answer.decision === "allow" ? `${answer.always ? "allowed (always)" : "allowed"} in the topic${by}` : `denied${by === "" ? " (or no answer)" : by}`);
117584
+ return answer;
117585
+ }
117586
+ async reportRemembered(approvalId, remembered) {
117587
+ if (approvalId === undefined)
117588
+ return;
117589
+ await this.client?.reportApprovalResult?.(approvalId, remembered);
116761
117590
  }
116762
117591
  }
116763
- var TG_SOURCE = "tg", REMOTE_EVENT_LIMIT = 20, REMOTE_REPLY_MAX_CHARS = 16000, EVENT_PREVIEW_CHARS = 80, APPROVAL_INPUT_PREVIEW_CHARS = 1500;
117592
+ var TG_SOURCE = "tg", REMOTE_EVENT_LIMIT = 20, REMOTE_REPLY_MAX_CHARS = 16000, EVENT_PREVIEW_CHARS = 80;
116764
117593
  var init_shell_bridge = __esm(() => {
116765
117594
  init_service2();
116766
117595
  init_client3();
@@ -118152,7 +118981,7 @@ function formatAge(ms) {
118152
118981
  return `${minutes}m`;
118153
118982
  return `${Math.round(minutes / 60)}h`;
118154
118983
  }
118155
- function formatRemoteStatusLines(status) {
118984
+ function formatRemoteStatusLines(status, posture) {
118156
118985
  const lines = [`Remote control: ${STATE_WORDS[status.state]}`];
118157
118986
  if (status.state === "off") {
118158
118987
  lines.push("Topic: none");
@@ -118168,6 +118997,9 @@ function formatRemoteStatusLines(status) {
118168
118997
  lines.push(`Approvals not confirmed: ${n}. The topic shows ${n === 1 ? "it" : "them"} as "not confirmed" until serve takes the shell's ack.`);
118169
118998
  }
118170
118999
  lines.push(`Turn it off with ${REMOTE_CONTROL_COMMAND} off, or press o here. Rename: ${REMOTE_CONTROL_COMMAND} off, then ${REMOTE_CONTROL_COMMAND} <name>.`);
119000
+ if (posture !== undefined && posture.length > 0) {
119001
+ lines.push("", "Telegram permissions (change with /remote-policy):", ...posture.map((line) => ` ${line}`));
119002
+ }
118171
119003
  return lines;
118172
119004
  }
118173
119005
  function clock2(at) {
@@ -118224,7 +119056,7 @@ function presentRemoteControl(open, otui, chrome, options) {
118224
119056
  return formatRemoteEventLines(status);
118225
119057
  if (tab === "commands")
118226
119058
  return formatRemoteCommandLines(status);
118227
- return formatRemoteStatusLines(status);
119059
+ return formatRemoteStatusLines(status, options.getPostureLines?.());
118228
119060
  };
118229
119061
  const content = (tab) => {
118230
119062
  const lines = linesFor(tab);
@@ -118293,6 +119125,13 @@ function projectRemoteRow(status, width) {
118293
119125
  return { text: fit3(`${topic} \xB7 ${unconfirmed} unconfirmed`, width), role: "attention", action: "open" };
118294
119126
  return { text: fit3(topic, width), role: "ok", action: "open" };
118295
119127
  }
119128
+ function projectPostureLine(status, posture, width) {
119129
+ if (status === undefined || status.state === "off" || posture === undefined || posture.length === 0)
119130
+ return;
119131
+ return posture.split(`
119132
+ `).map((line) => fit3(line, width)).join(`
119133
+ `);
119134
+ }
118296
119135
  function mountRemotePanel(otui, renderer, parent, options) {
118297
119136
  const core = otui;
118298
119137
  const box = new core.BoxRenderable(renderer, { id: "sb-remote", flexDirection: "column", flexShrink: 0 });
@@ -118306,6 +119145,8 @@ function mountRemotePanel(otui, renderer, parent, options) {
118306
119145
  const value = new core.TextRenderable(renderer, { id: "sb-remote-v", content: "" });
118307
119146
  box.add(label);
118308
119147
  box.add(value);
119148
+ const posture = new core.TextRenderable(renderer, { id: "sb-remote-p", content: "" });
119149
+ box.add(posture);
118309
119150
  const currentRow = () => projectRemoteRow(options.getStatus(), options.width);
118310
119151
  const draw = () => {
118311
119152
  if (disposed)
@@ -118313,6 +119154,9 @@ function mountRemotePanel(otui, renderer, parent, options) {
118313
119154
  const row = currentRow();
118314
119155
  label.content = core.t`${dimChunk(core, "Remote")}`;
118315
119156
  value.content = core.t`${roleChunk(core, row.role, row.text)}`;
119157
+ const line = projectPostureLine(options.getStatus(), options.getPosture?.(), options.width);
119158
+ posture.content = line === undefined ? "" : core.t`${dimChunk(core, line)}`;
119159
+ posture.visible = line !== undefined;
118316
119160
  };
118317
119161
  const activate = () => {
118318
119162
  options.onOpen();
@@ -118324,6 +119168,9 @@ function mountRemotePanel(otui, renderer, parent, options) {
118324
119168
  value.onMouseDown = () => {
118325
119169
  activate();
118326
119170
  };
119171
+ posture.onMouseDown = () => {
119172
+ options.onOpenPermissions?.();
119173
+ };
118327
119174
  const unsubscribeTheme = onThemeChange(guardedThemeRepaint("remote-panel", draw, () => disposed || isRenderableGone(box)));
118328
119175
  draw();
118329
119176
  return {
@@ -118587,6 +119434,11 @@ var init_agent_commands = __esm(() => {
118587
119434
  description: "How Telegram replies are written: /rendering [auto|rich|html|plain]",
118588
119435
  modes: AGENT_ONLY
118589
119436
  },
119437
+ {
119438
+ name: "/remote-policy",
119439
+ description: "Telegram defaults: /remote-policy [mode ask|trust] [limit none|<minutes>] [wait <minutes>]. Never changes the shell's /mode",
119440
+ modes: AGENT_ONLY
119441
+ },
118590
119442
  {
118591
119443
  name: "/product",
118592
119444
  description: "Intents closed in code that nobody looked back at, with their outcome criteria",
@@ -118622,6 +119474,11 @@ var init_agent_commands = __esm(() => {
118622
119474
  description: "Pending remote approvals: summary, scope, consequence, expiry \u2014 allow or deny one call, once",
118623
119475
  modes: AGENT_ONLY
118624
119476
  },
119477
+ {
119478
+ name: "/permissions",
119479
+ description: "The saved shell rules (what Always remembered): honoured or not, session grants; remove one",
119480
+ modes: AGENT_ONLY
119481
+ },
118625
119482
  {
118626
119483
  name: "/decisions",
118627
119484
  description: "Recommendation journal: how often you follow the agent's recommendation, by mode and stage, and the deviations; /decisions reason <why> and /decisions change <option> follow up on the last answer",
@@ -118832,10 +119689,14 @@ function classifyBusyDispatch(params) {
118832
119689
  return "remote-control";
118833
119690
  if (commandName === "/channels")
118834
119691
  return "channels";
119692
+ if (commandName === "/remote-policy")
119693
+ return "remote-policy";
118835
119694
  if (commandName === "/schedules")
118836
119695
  return "schedules";
118837
119696
  if (commandName === "/approvals")
118838
119697
  return "approvals";
119698
+ if (commandName === "/permissions")
119699
+ return "permissions";
118839
119700
  if (commandName === "/decisions")
118840
119701
  return "decisions";
118841
119702
  if (commandName === "/external-diff")
@@ -138664,6 +139525,10 @@ async function runSettingsCommand(command, handlers) {
138664
139525
  case "/theme":
138665
139526
  handlers.theme(arg);
138666
139527
  return;
139528
+ case "/remote-policy":
139529
+ if (handlers.remotePolicy !== undefined)
139530
+ handlers.onSystem(handlers.remotePolicy(arg));
139531
+ return;
138667
139532
  }
138668
139533
  } catch (error) {
138669
139534
  handlers.onSystem(`${name}: ${error instanceof Error ? error.message : String(error)}
@@ -139738,6 +140603,58 @@ var init_games = __esm(() => {
139738
140603
  init_tic_tac_toe();
139739
140604
  });
139740
140605
 
140606
+ // src/remote/telegram-permission.ts
140607
+ function modeInForce(input) {
140608
+ if (input.changedThisSession)
140609
+ return { mode: input.shellMode, source: "shell-mode" };
140610
+ if (input.telegramTurn)
140611
+ return { mode: input.telegramDefault, source: "telegram-default" };
140612
+ return { mode: input.shellMode, source: "shell-default" };
140613
+ }
140614
+ function formatModeInForce(value) {
140615
+ const source = value.source === "telegram-default" ? "Telegram default" : value.source === "shell-mode" ? "shell /mode" : "shell default";
140616
+ return `${value.mode} (${source})`;
140617
+ }
140618
+ function telegramRememberOffer(ev) {
140619
+ if (ev.destructive || ev.credentials || ev.sacReviewConfirmation || ev.publishLease || ev.hookAsk || ev.untrustedOrigin) {
140620
+ return;
140621
+ }
140622
+ if (ev.command.length > APPROVAL_INPUT_PREVIEW_CHARS)
140623
+ return;
140624
+ const { exact, prefix, offerExact, offerPrefix } = suggestShellPatterns(ev.command);
140625
+ const shownAsStored = (pattern) => redactSensitiveText(pattern) === pattern;
140626
+ if (offerExact) {
140627
+ return exact.length <= MAX_REMEMBER_PATTERN_CHARS && shownAsStored(exact) ? { pattern: exact, kind: "exact" } : undefined;
140628
+ }
140629
+ if (offerPrefix && prefix.length <= MAX_REMEMBER_PATTERN_CHARS && shownAsStored(prefix))
140630
+ return { pattern: prefix, kind: "prefix" };
140631
+ return;
140632
+ }
140633
+ function evaluateTelegramShellApproval(input) {
140634
+ const evaluation = evaluateShellApproval(input);
140635
+ return {
140636
+ evaluation,
140637
+ autoApprove: evaluation.autoApprove,
140638
+ offer: evaluation.autoApprove ? undefined : telegramRememberOffer(evaluation)
140639
+ };
140640
+ }
140641
+ function userSuffix(userId) {
140642
+ return userId === undefined ? "" : `, user ${userId}`;
140643
+ }
140644
+ function modeAutoApprovalAudit(input) {
140645
+ return `auto-approved (${input.mode}${userSuffix(input.userId)}): ${input.preview}`;
140646
+ }
140647
+ function savedRuleAutoApprovalAudit(input) {
140648
+ const shown = input.command.length > 200 ? `${input.command.slice(0, 199)}\u2026` : input.command;
140649
+ return `auto-approved by a saved rule${input.userId === undefined ? "" : ` (user ${input.userId})`}: ${shown}`;
140650
+ }
140651
+ var init_telegram_permission = __esm(() => {
140652
+ init_shell_approval();
140653
+ init_shell_permissions();
140654
+ init_service2();
140655
+ init_protocol2();
140656
+ });
140657
+
139741
140658
  // src/mcp-client/elicitation.ts
139742
140659
  function extractCodexCommand(vendor) {
139743
140660
  const raw = vendor.codex_command;
@@ -141383,8 +142300,12 @@ async function forceForegroundQueueItem(owner, handoff, item, deps) {
141383
142300
  }
141384
142301
  });
141385
142302
  }
141386
- function createForegroundAgentIoFacade(owner, token, io) {
142303
+ function createForegroundAgentIoFacade(owner, token, io, turn = {}) {
141387
142304
  const accepts = () => owner.accepts(token);
142305
+ const approve = () => turn.requestApproval ?? io.requestApproval;
142306
+ const autoApproved = () => turn.onAutoApproved ?? io.onAutoApproved;
142307
+ const modeOf = () => turn.permissionMode ?? io.permissionMode;
142308
+ const mcpGrantsApply = turn.mcpGrantsApply ?? io.mcpGrantsApply;
141388
142309
  return {
141389
142310
  write: (text) => {
141390
142311
  if (accepts())
@@ -141433,15 +142354,16 @@ function createForegroundAgentIoFacade(owner, token, io) {
141433
142354
  requestApproval: async (tool, input, meta) => {
141434
142355
  if (!accepts())
141435
142356
  return false;
141436
- const response = await io.requestApproval?.(tool, input, meta);
142357
+ const response = await approve()?.(tool, input, meta);
141437
142358
  return accepts() ? response ?? false : false;
141438
142359
  },
141439
142360
  onAutoApproved: (tool, input, meta) => {
141440
142361
  if (accepts())
141441
- io.onAutoApproved?.(tool, input, meta);
142362
+ autoApproved()?.(tool, input, meta);
141442
142363
  },
141443
- permissionMode: () => accepts() ? io.permissionMode?.() ?? "ask" : "ask",
142364
+ permissionMode: () => accepts() ? modeOf()?.() ?? "ask" : "ask",
141444
142365
  ...io.trustedMcpTools === undefined ? {} : { trustedMcpTools: io.trustedMcpTools },
142366
+ ...mcpGrantsApply === undefined ? {} : { mcpGrantsApply },
141445
142367
  ...io.mcpToolFingerprint === undefined ? {} : { mcpToolFingerprint: io.mcpToolFingerprint },
141446
142368
  ...io.mcpToolDestructive === undefined ? {} : { mcpToolDestructive: io.mcpToolDestructive },
141447
142369
  ...io.beforeMutation === undefined ? {} : {
@@ -151349,7 +152271,7 @@ async function launchTuiAgentShell(opts) {
151349
152271
  });
151350
152272
  let uid = 0;
151351
152273
  const herdr = createHerdrReporter();
151352
- const sessionShellAllow = new Set(loadShellPermissions().allow);
152274
+ const sessionShellAllow = new Set;
151353
152275
  let permissionMigrationShown = false;
151354
152276
  let permissionsFingerprintAtStart = shellPermissionsFingerprint();
151355
152277
  let permissionTamperShown = false;
@@ -151694,7 +152616,12 @@ async function launchTuiAgentShell(opts) {
151694
152616
  liveRemotePanel = mountRemotePanel(otui, r, sidebar, {
151695
152617
  width: SIDEBAR_TEXT_WIDTH,
151696
152618
  getStatus: remoteStatus,
151697
- onOpen: () => showRemoteControl()
152619
+ onOpen: () => showRemoteControl(),
152620
+ onOpenPermissions: () => showPermissions(),
152621
+ getPosture: () => {
152622
+ const posture = remoteBridge?.active === true ? telegramPosture() : undefined;
152623
+ return posture === undefined ? undefined : postureSidebarText(posture);
152624
+ }
151698
152625
  });
151699
152626
  liveChannelsPanel = mountChannelsPanel(otui, r, sidebar, {
151700
152627
  width: SIDEBAR_TEXT_WIDTH,
@@ -152509,24 +153436,60 @@ async function launchTuiAgentShell(opts) {
152509
153436
  setBusyPhase("running approved shell");
152510
153437
  return true;
152511
153438
  };
152512
- io.requestApproval = async (tool, inputJson, meta) => {
153439
+ const requestApprovalForTelegramTurn = async (tool, inputJson, meta) => {
152513
153440
  const bridge = remoteBridge;
152514
153441
  if (bridge === undefined || !bridge.telegramTurnActive) {
152515
153442
  return requestApprovalLocally(tool, inputJson, meta);
152516
153443
  }
152517
153444
  const brief = tool.length > 60 ? `${tool.slice(0, 59)}\u2026` : tool;
153445
+ const approvedAnswer = () => meta?.fingerprint !== undefined ? { approved: true, fingerprint: meta.fingerprint } : true;
153446
+ let offer;
153447
+ if (tool === "shell_exec") {
153448
+ const judged = evaluateTelegramShellApproval({
153449
+ inputJson,
153450
+ ...meta !== undefined ? { meta } : {},
153451
+ sessionAllow: sessionShellAllow,
153452
+ fingerprintAtStart: permissionsFingerprintAtStart
153453
+ });
153454
+ if (!permissionTamperShown && judged.evaluation.tampered) {
153455
+ permissionTamperShown = true;
153456
+ io.onSystem?.(`\u26A0 the saved shell permissions changed outside this approval UI \u2014 review them before trusting an auto-approve
153457
+ `);
153458
+ }
153459
+ if (judged.autoApprove) {
153460
+ const shown = judged.evaluation.command.length > 200 ? `${judged.evaluation.command.slice(0, 199)}\u2026` : judged.evaluation.command;
153461
+ io.onSystem?.(`\u2713 auto-approved shell (saved rule): ${shown}
153462
+ `);
153463
+ bridge.recordApproval(savedRuleAutoApprovalAudit({ userId: bridge.telegramTurnUserId, command: judged.evaluation.command }));
153464
+ return approvedAnswer();
153465
+ }
153466
+ offer = judged.offer;
153467
+ }
152518
153468
  io.onSystem?.(`\u25C7 approval for ${brief} sent to the Telegram topic; waiting for the answer there.
152519
153469
  `);
152520
153470
  setMainAgent("blocked", "approval (telegram)");
152521
- const decision = await bridge.requestApproval(describeApprovalForTopic(tool, inputJson, meta));
153471
+ const answer = await bridge.askApproval(describeApprovalForTopic(tool, inputJson, meta), offer === undefined ? {} : { remember: offer.pattern });
153472
+ const decision = answer.decision;
153473
+ let rememberedNote = "";
153474
+ if (decision === "allow" && answer.always && offer !== undefined) {
153475
+ const stored = allowShellPattern(offer.pattern);
153476
+ if (stored.length > 0) {
153477
+ sessionShellAllow.add(stored);
153478
+ permissionsFingerprintAtStart = shellPermissionsFingerprint();
153479
+ }
153480
+ rememberedNote = stored.length > 0 ? ` and remembered \u201C${stored}\u201D` : ` once; \u201C${offer.pattern}\u201D cannot be remembered`;
153481
+ bridge.recordApproval(stored.length > 0 ? `saved a rule: ${stored}` : "a rule was asked for and refused by the store");
153482
+ await bridge.reportRemembered(answer.approvalId, stored.length > 0);
153483
+ }
152522
153484
  setMainAgent("running", decision === "allow" ? "approved (telegram)" : "denied");
152523
- io.onSystem?.(decision === "allow" ? `\u25C7 ${brief} approved in Telegram.
153485
+ io.onSystem?.(decision === "allow" ? `\u25C7 ${brief} approved in Telegram${rememberedNote}.
152524
153486
  ` : `\u25C7 ${brief} denied (Telegram answer, timeout or no connection).
152525
153487
  `);
152526
153488
  if (decision !== "allow")
152527
153489
  return false;
152528
- return meta?.fingerprint !== undefined ? { approved: true, fingerprint: meta.fingerprint } : true;
153490
+ return approvedAnswer();
152529
153491
  };
153492
+ io.requestApproval = requestApprovalLocally;
152530
153493
  const askUserInteractive = async (req) => {
152531
153494
  chrome.hideMenu();
152532
153495
  setMainAgent("blocked", "ask");
@@ -152607,7 +153570,15 @@ async function launchTuiAgentShell(opts) {
152607
153570
  });
152608
153571
  const sessionCwd = opts.session?.cwd ?? process.cwd();
152609
153572
  let permissionMode = opts.initialPermissionMode ?? getProjectPermissionMode(sessionCwd) ?? DEFAULT_PERMISSION_MODE;
152610
- io.permissionMode = () => permissionMode;
153573
+ let modeChangedThisSession = false;
153574
+ const modeForTurn = (telegramTurn) => modeInForce({
153575
+ shellMode: permissionMode,
153576
+ changedThisSession: modeChangedThisSession,
153577
+ telegramTurn,
153578
+ telegramDefault: remoteBridge?.configuredPermissionMode ?? "ask"
153579
+ });
153580
+ const modeNow = () => modeForTurn(remoteBridge?.telegramTurnActive === true);
153581
+ io.permissionMode = () => modeForTurn(false).mode;
152611
153582
  io.trustedMcpTools = new Map;
152612
153583
  io.mcpToolFingerprint = (fqn) => catalogFingerprintResolver(deps.mcpRuntime?.()?.catalog())(fqn);
152613
153584
  io.mcpToolDestructive = (fqn) => catalogDestructiveResolver(deps.mcpRuntime?.()?.catalog())(fqn);
@@ -152615,18 +153586,30 @@ async function launchTuiAgentShell(opts) {
152615
153586
  io.readOnly = () => readOnly;
152616
153587
  const paintModeRow = () => {
152617
153588
  const row = describeModeRow(permissionMode, readOnly);
153589
+ const inForce = remoteBridge?.active === true ? formatModeInForce(modeNow()) : undefined;
152618
153590
  const hint = settingsSidebarHint(row.readOnly !== undefined);
152619
- sbModeV.content = row.readOnly !== undefined ? otui.t`${dimChunk(otui, `mode ${row.mode}`)} ${roleChunk(otui, "attention", row.readOnly)}` : otui.t`${dimChunk(otui, `mode ${row.mode}${hint === undefined ? "" : ` ${hint}`}`)}`;
153591
+ sbModeV.content = row.readOnly !== undefined ? otui.t`${dimChunk(otui, `mode ${inForce ?? row.mode}`)} ${roleChunk(otui, "attention", row.readOnly)}` : otui.t`${dimChunk(otui, `mode ${inForce ?? row.mode}${hint === undefined || inForce !== undefined ? "" : ` ${hint}`}`)}`;
152620
153592
  };
152621
153593
  paintModeRow();
152622
- io.onAutoApproved = (tool, input, meta) => {
153594
+ const autoApprovedFor = (telegramTurn) => (tool, input, meta) => {
152623
153595
  const preview = tool === "shell_exec" ? parseShellExecCommand(input) : isMcpToolCall(tool) ? `${tool} ${describeUseToolApproval(input, catalogResolver(deps.mcpRuntime?.()?.catalog())).fqn}` : tool;
152624
- const label = `\u25C7 auto-approved (${permissionMode})` + (meta.destructive ? " [destructive]" : "") + (meta.mcpTrusted === true ? ` ${TRUSTED_MARKER}` : "");
153596
+ const inForce = modeForTurn(telegramTurn);
153597
+ const label = `\u25C7 auto-approved (${inForce.mode})` + (meta.destructive ? " [destructive]" : "") + (meta.mcpTrusted === true ? ` ${TRUSTED_MARKER}` : "");
153598
+ if (telegramTurn && remoteBridge !== undefined) {
153599
+ remoteBridge.recordApproval(modeAutoApprovalAudit({ mode: inForce.mode, userId: remoteBridge.telegramTurnUserId, preview }));
153600
+ }
152625
153601
  transcript.add(new otui.TextRenderable(r, {
152626
153602
  id: `ap${uid++}`,
152627
153603
  content: otui.t`${roleChunk(otui, "attention", label)} ${dimChunk(otui, preview)}`
152628
153604
  }));
152629
153605
  };
153606
+ io.onAutoApproved = autoApprovedFor(false);
153607
+ const telegramTurnIo = {
153608
+ requestApproval: requestApprovalForTelegramTurn,
153609
+ permissionMode: () => modeForTurn(true).mode,
153610
+ onAutoApproved: autoApprovedFor(true),
153611
+ mcpGrantsApply: () => false
153612
+ };
152630
153613
  let liveSession;
152631
153614
  let history = [];
152632
153615
  let archive = [];
@@ -153140,6 +154123,50 @@ Staying in the current session.
153140
154123
  io.onSystem?.("The product index could not be used. Run `keryx product index`.\n");
153141
154124
  });
153142
154125
  };
154126
+ const permissionsChanged = () => {
154127
+ permissionsFingerprintAtStart = shellPermissionsFingerprint();
154128
+ };
154129
+ const showPermissions = () => {
154130
+ openPermissions(otui, chrome, {
154131
+ sessionAllow: sessionShellAllow,
154132
+ onChanged: permissionsChanged,
154133
+ onKeypress: (handler) => onKeypress4(r, handler),
154134
+ renderer: r,
154135
+ inputBlocked: () => chrome.keyboardOwnedElsewhere()
154136
+ });
154137
+ };
154138
+ const runPermissionsCommand = (line) => {
154139
+ const rest = line.trim().slice(PERMISSIONS_COMMAND.length).trim();
154140
+ if (rest.length === 0) {
154141
+ showPermissions();
154142
+ return;
154143
+ }
154144
+ io.onSystem?.(permissionsSlashText(rest, { sessionAllow: sessionShellAllow, onChanged: permissionsChanged }));
154145
+ };
154146
+ const runningTelegramPolicy = () => {
154147
+ const bridge = remoteBridge;
154148
+ if (bridge === undefined || !bridge.active)
154149
+ return;
154150
+ return {
154151
+ defaultMode: bridge.configuredPermissionMode,
154152
+ runTimeoutMs: bridge.runTimeoutMs,
154153
+ approvalTimeoutMs: bridge.approvalTimeoutMs
154154
+ };
154155
+ };
154156
+ const telegramModeInForce = () => remoteBridge?.active === true ? formatModeInForce(modeNow()) : undefined;
154157
+ const remotePolicyDeps = () => ({
154158
+ running: runningTelegramPolicy,
154159
+ inForce: telegramModeInForce,
154160
+ applyPolicy: (patch) => {
154161
+ remoteBridge?.applyPolicy(patch);
154162
+ paintModeRow();
154163
+ liveRemotePanel?.refresh();
154164
+ }
154165
+ });
154166
+ const telegramPosture = () => loadPosture({ running: runningTelegramPolicy, inForce: telegramModeInForce });
154167
+ const runRemotePolicyCommand = (line) => {
154168
+ io.onSystem?.(remotePolicyText(line.trim().slice(REMOTE_POLICY_COMMAND.length), remotePolicyDeps()));
154169
+ };
153143
154170
  const showReviews = () => {
153144
154171
  openReviews(otui, chrome, { cwd: inspectorCwd(), renderer: r, ...inspectorKeys }).then(() => liveReviewsPanel?.refresh()).catch(() => {
153145
154172
  io.onSystem?.(`The managed reviews could not be read.
@@ -153170,6 +154197,10 @@ Staying in the current session.
153170
154197
  const showRemoteControl = () => {
153171
154198
  openRemoteControl(otui, chrome, {
153172
154199
  getStatus: remoteStatus,
154200
+ getPostureLines: () => {
154201
+ const posture = remoteBridge?.active === true ? telegramPosture() : undefined;
154202
+ return posture === undefined ? undefined : postureLines(posture);
154203
+ },
153173
154204
  onToggle: () => {
153174
154205
  remoteBridge?.active === true ? disableRemoteControl() : enableRemoteControl();
153175
154206
  },
@@ -153567,6 +154598,7 @@ Staying in the current session.
153567
154598
  };
153568
154599
  const commitPermissionMode = (next) => {
153569
154600
  permissionMode = next;
154601
+ modeChangedThisSession = true;
153570
154602
  paintModeRow();
153571
154603
  chrome.showToast(`Permission mode: ${next}`);
153572
154604
  };
@@ -153737,7 +154769,12 @@ ${formatThemeList(getThemeId())}`);
153737
154769
  guard: guardEnabled,
153738
154770
  routing: routingEnabled,
153739
154771
  thinkDisplay: thinkDisplayMode,
153740
- reasoningOverride
154772
+ reasoningOverride,
154773
+ ...(() => {
154774
+ const running = runningTelegramPolicy();
154775
+ const inForce = telegramModeInForce();
154776
+ return running === undefined ? {} : { telegram: { ...running, ...inForce !== undefined ? { inForce } : {} } };
154777
+ })()
153741
154778
  }));
153742
154779
  const runSettingsAction = (command) => runSettingsCommand(command, {
153743
154780
  mode: runModeCommand,
@@ -153752,6 +154789,7 @@ ${formatThemeList(getThemeId())}`);
153752
154789
  reasoning: runReasoningCommand,
153753
154790
  think: runThinkCommand,
153754
154791
  theme: runThemeCommand,
154792
+ remotePolicy: (arg) => remotePolicyText(arg, remotePolicyDeps()),
153755
154793
  onSystem: (text) => io.onSystem?.(text)
153756
154794
  });
153757
154795
  const showSettings = () => {
@@ -154294,6 +155332,14 @@ ${formatThemeList(getThemeId())}`);
154294
155332
  routeApprovalsCommand(line, true, approvals);
154295
155333
  return;
154296
155334
  }
155335
+ case "permissions": {
155336
+ runPermissionsCommand(line);
155337
+ return;
155338
+ }
155339
+ case "remote-policy": {
155340
+ runRemotePolicyCommand(line);
155341
+ return;
155342
+ }
154297
155343
  case "decisions": {
154298
155344
  routeDecisionsCommand(line, decisionsPanel);
154299
155345
  return;
@@ -154618,6 +155664,14 @@ ${formatThemeList(getThemeId())}`);
154618
155664
  if (routeApprovalsCommand(line, false, approvals)) {
154619
155665
  return;
154620
155666
  }
155667
+ if (command.name === PERMISSIONS_COMMAND) {
155668
+ runPermissionsCommand(line);
155669
+ return;
155670
+ }
155671
+ if (command.name === REMOTE_POLICY_COMMAND) {
155672
+ runRemotePolicyCommand(line);
155673
+ return;
155674
+ }
154621
155675
  if (routeDecisionsCommand(line, decisionsPanel)) {
154622
155676
  return;
154623
155677
  }
@@ -155160,6 +156214,7 @@ ${formatThemeList(getThemeId())}`);
155160
156214
  deps.resetSubagentBudget?.();
155161
156215
  const operation = foregroundOperation.begin();
155162
156216
  remoteQueue.turnStarted(source);
156217
+ const telegramTurn = source === TG_SOURCE && remoteBridge?.telegramTurnActive === true;
155163
156218
  blockIo.resetReasoningLiveState();
155164
156219
  setMainAgent("running", "waiting");
155165
156220
  startBusy("waiting for model");
@@ -155246,7 +156301,7 @@ ${formatThemeList(getThemeId())}`);
155246
156301
  guardCollector.reset(line);
155247
156302
  syncArchive();
155248
156303
  rewindRecorder.beginTurn({ archiveIndex: archive.length, prompt: line });
155249
- const foregroundIo = createForegroundAgentIoFacade(foregroundOperation, operation, io);
156304
+ const foregroundIo = createForegroundAgentIoFacade(foregroundOperation, operation, io, telegramTurn ? telegramTurnIo : {});
155250
156305
  const turnSignal = foregroundOperation.signal;
155251
156306
  runAgentTurn(foregroundIo, deps, history, line, {
155252
156307
  signal: turnSignal,
@@ -155434,7 +156489,7 @@ ${formatThemeList(getThemeId())}`);
155434
156489
  });
155435
156490
  return `${formatSessionInfoText(snapshot).trimEnd()}
155436
156491
 
155437
- Mode: ${permissionMode}${readOnly ? " (read-only on)" : ""}
156492
+ Mode: ${formatModeInForce(modeForTurn(true))}${readOnly ? " (read-only on)" : ""}
155438
156493
  Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} waiting`;
155439
156494
  };
155440
156495
  const runRemoteCommand = async (line) => {
@@ -155447,12 +156502,13 @@ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} wa
155447
156502
  `);
155448
156503
  });
155449
156504
  }
155450
- if (name === "/mode" && (first === "" || first === "auto")) {
156505
+ const modeWords = line.trim().split(/\s+/).slice(1).filter((word) => word.length > 0);
156506
+ if (name === "/mode" && (first === "" || (first === "ask" || first === "trust" || first === "auto") && modeWords.length === 1)) {
155451
156507
  echoRemoteCommand(line);
155452
156508
  return await captureShellOutput(() => {
155453
- if (first === "auto")
155454
- commitPermissionMode("auto");
155455
- io.onSystem?.(`Permission mode: ${permissionMode} (ask, trust or auto)
156509
+ if (first === "ask" || first === "trust" || first === "auto")
156510
+ commitPermissionMode(first);
156511
+ io.onSystem?.(`Permission mode: ${formatModeInForce(modeForTurn(true))} (ask, trust or auto)
155456
156512
  `);
155457
156513
  });
155458
156514
  }
@@ -155555,7 +156611,10 @@ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} wa
155555
156611
  pendingResumeTarget = undefined;
155556
156612
  return liveSession.summary.id === found.id ? { ...outcome, ok: true } : { ...outcome, ok: false };
155557
156613
  },
155558
- onChange: () => liveRemotePanel?.refresh()
156614
+ onChange: () => {
156615
+ liveRemotePanel?.refresh();
156616
+ paintModeRow();
156617
+ }
155559
156618
  }
155560
156619
  });
155561
156620
  onKeypress4(r, (key) => {
@@ -155607,6 +156666,9 @@ var init_tui_shell = __esm(() => {
155607
156666
  init_flow_inspector();
155608
156667
  init_product_open_surface();
155609
156668
  init_reviews_inspector();
156669
+ init_permissions_command();
156670
+ init_remote_policy_command();
156671
+ init_permissions_inspector();
155610
156672
  init_shell_bridge();
155611
156673
  init_command_gateway();
155612
156674
  init_channels_client();
@@ -155696,6 +156758,7 @@ var init_tui_shell = __esm(() => {
155696
156758
  init_provider_config();
155697
156759
  init_shell_permissions();
155698
156760
  init_shell_approval();
156761
+ init_telegram_permission();
155699
156762
  init_elicitation();
155700
156763
  init_permission_mode_config();
155701
156764
  init_permission_mode();
@@ -157536,7 +158599,7 @@ async function runAgentRepl(lines, rich, deps, metaprojectPort, sessionOpts, ini
157536
158599
  }
157537
158600
  };
157538
158601
  const searchProviderController = createDefaultSearchProviderController();
157539
- const sessionShellAllow = new Set(loadShellPermissions(configDir).allow);
158602
+ const sessionShellAllow = new Set;
157540
158603
  let fingerprintAtStart = shellPermissionsFingerprint(configDir);
157541
158604
  let permissionMigrationShown = false;
157542
158605
  let permissionTamperShown = false;
@@ -158228,6 +159291,8 @@ ${GUTTER}${turnSeparator()}
158228
159291
  agentIo.onSystem?.(readlineChannelsText(line, await loadChannelsSnapshot(new ChannelsClient)));
158229
159292
  } else if (isRenderingCommand(command)) {
158230
159293
  agentIo.onSystem?.(runRenderingCommand(rest));
159294
+ } else if (command === "/remote-policy") {
159295
+ agentIo.onSystem?.(remotePolicyText(rest, { ...configDir !== undefined ? { dir: configDir } : {} }));
158231
159296
  } else if (isSessionInfoCommand(command)) {
158232
159297
  const cwd = sessionCwd;
158233
159298
  const [workspaces, flows] = await Promise.all([
@@ -158251,6 +159316,13 @@ ${GUTTER}${turnSeparator()}
158251
159316
  })));
158252
159317
  } else if (command === "/approvals") {
158253
159318
  agentIo.onSystem?.(approvalsSlashText(rest));
159319
+ } else if (command === "/permissions") {
159320
+ agentIo.onSystem?.(permissionsSlashText(rest, {
159321
+ sessionAllow: sessionShellAllow,
159322
+ onChanged: () => {
159323
+ fingerprintAtStart = shellPermissionsFingerprint(configDir);
159324
+ }
159325
+ }));
158254
159326
  } else if (command === "/external-diff") {
158255
159327
  agentIo.onSystem?.(externalDiffSlashText(defaultExternalDiffDeps(sessionCwd)));
158256
159328
  } else if (isFlowsCommand(command)) {
@@ -159359,6 +160431,8 @@ var init_shell = __esm(() => {
159359
160431
  init_flow_inspector();
159360
160432
  init_inspector_sources();
159361
160433
  init_approvals();
160434
+ init_permissions_command();
160435
+ init_remote_policy_command();
159362
160436
  init_external_diff_modal();
159363
160437
  init_doctor2();
159364
160438
  init_setup_guide();
@@ -159425,6 +160499,7 @@ var init_shell = __esm(() => {
159425
160499
  "/status",
159426
160500
  "/flows",
159427
160501
  "/approvals",
160502
+ "/permissions",
159428
160503
  "/external-diff",
159429
160504
  "/doctor",
159430
160505
  "/setup",
@@ -159432,6 +160507,7 @@ var init_shell = __esm(() => {
159432
160507
  "/remote-control",
159433
160508
  "/channels",
159434
160509
  "/rendering",
160510
+ "/remote-policy",
159435
160511
  "/theme",
159436
160512
  "/settings",
159437
160513
  "/mode",
@@ -193371,6 +194447,7 @@ var init_serve_server = __esm(() => {
193371
194447
  ["heartbeat", "POST"],
193372
194448
  ["reply", "POST"],
193373
194449
  ["approval", "POST"],
194450
+ ["approval-result", "POST"],
193374
194451
  ["prompt", "POST"],
193375
194452
  ["state", "POST"],
193376
194453
  ["ack", "POST"],
@@ -196086,7 +197163,12 @@ class RemoteHub {
196086
197163
  }));
196087
197164
  }
196088
197165
  limits() {
196089
- return { runTimeoutMs: this.config.runTimeoutMs, orphanMs: this.config.orphanMs };
197166
+ return {
197167
+ runTimeoutMs: this.config.runTimeoutMs,
197168
+ orphanMs: this.config.orphanMs,
197169
+ permissionMode: this.config.permissionMode,
197170
+ approvalTimeoutMs: this.config.approvalTimeoutMs
197171
+ };
196090
197172
  }
196091
197173
  hasSession(sessionId) {
196092
197174
  return this.registry.bySession(sessionId) !== undefined;
@@ -196942,6 +198024,8 @@ class RemoteHttpSurface {
196942
198024
  return this.reply(hub, request);
196943
198025
  case "approval":
196944
198026
  return this.requestApproval(hub, request);
198027
+ case "approval-result":
198028
+ return this.approvalResult(hub, request);
196945
198029
  case "prompt":
196946
198030
  return this.requestPrompt(hub, request);
196947
198031
  case "state":
@@ -197005,7 +198089,15 @@ class RemoteHttpSurface {
197005
198089
  this.sent.delete(checked.sessionId);
197006
198090
  this.bound.set(checked.sessionId, binding);
197007
198091
  }
197008
- return ok2({ name: result.name, threadId: result.threadId, reused: result.reused, runTimeoutMs: hub.limits().runTimeoutMs });
198092
+ const limits = hub.limits();
198093
+ return ok2({
198094
+ name: result.name,
198095
+ threadId: result.threadId,
198096
+ reused: result.reused,
198097
+ runTimeoutMs: limits.runTimeoutMs,
198098
+ permissionMode: limits.permissionMode,
198099
+ approvalTimeoutMs: limits.approvalTimeoutMs
198100
+ });
197009
198101
  }
197010
198102
  const status = result.code === "name-taken" ? 409 : result.code === "invalid-name" ? 400 : 502;
197011
198103
  return fail4(status, result.code, result.message);
@@ -197132,6 +198224,13 @@ class RemoteHttpSurface {
197132
198224
  if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < MIN_APPROVAL_TIMEOUT_MS || timeoutMs > MAX_APPROVAL_TIMEOUT_MS) {
197133
198225
  return invalid2(`timeoutMs must be an integer from ${MIN_APPROVAL_TIMEOUT_MS} to ${MAX_APPROVAL_TIMEOUT_MS}.`);
197134
198226
  }
198227
+ let remember;
198228
+ if (value.remember !== undefined) {
198229
+ if (typeof value.remember !== "string" || value.remember.length === 0 || value.remember.length > MAX_REMEMBER_PATTERN_CHARS || CONTROL_CHARS2.test(value.remember)) {
198230
+ return invalid2(`remember must be a string of 1 to ${MAX_REMEMBER_PATTERN_CHARS} characters without control characters.`);
198231
+ }
198232
+ remember = redactSensitiveText(value.remember);
198233
+ }
197135
198234
  const sessionId = value.sessionId;
197136
198235
  if (!hub.hasSession(sessionId)) {
197137
198236
  return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
@@ -197153,15 +198252,20 @@ class RemoteHttpSurface {
197153
198252
  this.approvals.set(approvalId, {
197154
198253
  sessionId,
197155
198254
  expiresAt,
197156
- timer: this.timers.setTimeout(() => this.expireApproval(approvalId), timeoutMs)
198255
+ timer: this.timers.setTimeout(() => this.expireApproval(approvalId), timeoutMs),
198256
+ ...remember === undefined ? {} : { remember }
197157
198257
  });
198258
+ const offer = remember === undefined ? "" : `
198259
+ Always would remember:
198260
+ ${asCodeBlock(remember)}`;
197158
198261
  const sent = await hub.send(sessionId, `Approval needed:
197159
- ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
198262
+ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
197160
198263
  keyboard: [
197161
198264
  [
197162
198265
  { text: "Allow", callback_data: approvalCallbackData(approvalId, "allow") },
197163
198266
  { text: "Deny", callback_data: approvalCallbackData(approvalId, "deny") }
197164
- ]
198267
+ ],
198268
+ ...remember === undefined ? [] : [[{ text: alwaysButtonText(remember), callback_data: approvalCallbackData(approvalId, "always") }]]
197165
198269
  ],
197166
198270
  onSent: (info) => this.onApprovalSent(approvalId, sessionId, info.messageId, info.text)
197167
198271
  });
@@ -197171,6 +198275,45 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
197171
198275
  }
197172
198276
  return ok2({ approvalId, expiresAt });
197173
198277
  }
198278
+ async approvalResult(hub, request) {
198279
+ const body = await readJsonBody(request);
198280
+ if (!body.ok) {
198281
+ return body.response;
198282
+ }
198283
+ const value = body.value;
198284
+ if (!isSessionId(value.sessionId)) {
198285
+ return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
198286
+ }
198287
+ if (!isApprovalId(value.approvalId) || typeof value.remembered !== "boolean") {
198288
+ return invalid2("approvalId must be an approval id and remembered a boolean.");
198289
+ }
198290
+ const waiting = this.awaiting.get(value.approvalId);
198291
+ if (waiting !== undefined && waiting.sessionId === value.sessionId && waiting.remember !== undefined) {
198292
+ waiting.reported ??= value.remembered;
198293
+ return ok2({ settled: true });
198294
+ }
198295
+ const done = this.finished.get(value.approvalId);
198296
+ if (done === undefined || done.sessionId !== value.sessionId || done.rememberOf === undefined) {
198297
+ return fail4(404, "unknown-approval", "No approval of this session is waiting for that report.");
198298
+ }
198299
+ this.settleRemembered(hub, value.sessionId, done, value.remembered);
198300
+ return ok2({ settled: true });
198301
+ }
198302
+ settleRemembered(hub, sessionId, done, remembered) {
198303
+ const info = done.rememberOf;
198304
+ if (info === undefined) {
198305
+ return;
198306
+ }
198307
+ done.rememberOf = undefined;
198308
+ const result = remembered ? `Allowed by ${info.who} at ${info.when}. Remembered: ${info.pattern}` : `Allowed by ${info.who} at ${info.when}. Not remembered: ${info.pattern} (approved this once).`;
198309
+ done.shortReply = remembered ? "Approval granted and remembered." : "Approval granted once; the rule was not remembered.";
198310
+ done.finalText = done.messageId === undefined ? result : settledText(info.original, result);
198311
+ if (done.messageId !== undefined) {
198312
+ hub.settleMessage(sessionId, done.messageId, done.finalText, done.shortReply).catch(() => {
198313
+ return;
198314
+ });
198315
+ }
198316
+ }
197174
198317
  async requestPrompt(hub, request) {
197175
198318
  const body = await readJsonBody(request);
197176
198319
  if (!body.ok) {
@@ -197320,11 +198463,25 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
197320
198463
  this.rememberDecided(id, sessionId);
197321
198464
  if (value.applied === false) {
197322
198465
  this.finishApproval(id, sessionId, waiting.messageId, waiting.text, {
197323
- result: `Not applied at ${waiting.sentAt}: the shell was no longer waiting for this question, so the ${waiting.decision === "allow" ? "Allow" : "Deny"} from ${waiting.who} changed nothing.`,
198466
+ result: `Not applied at ${waiting.sentAt}: the shell was no longer waiting for this question, so the ${waiting.decision === "deny" ? "Deny" : "Allow"} from ${waiting.who} changed nothing.`,
197324
198467
  shortReply: "Not applied: the shell was no longer waiting for this question."
197325
198468
  });
197326
198469
  return ok2({ acknowledged: true });
197327
198470
  }
198471
+ if (waiting.decision === "always" && waiting.remember !== undefined) {
198472
+ this.finishApproval(id, sessionId, waiting.messageId, waiting.text, {
198473
+ result: `Allowed by ${waiting.who} at ${waiting.sentAt}. Saving the rule: ${waiting.remember}`,
198474
+ shortReply: "Approval granted; saving the rule."
198475
+ });
198476
+ const done = this.finished.get(id);
198477
+ if (done !== undefined) {
198478
+ done.rememberOf = { original: waiting.text, who: waiting.who, when: waiting.sentAt, pattern: waiting.remember };
198479
+ if (waiting.reported !== undefined) {
198480
+ this.settleRemembered(hub, sessionId, done, waiting.reported);
198481
+ }
198482
+ }
198483
+ return ok2({ acknowledged: true });
198484
+ }
197328
198485
  this.finishApproval(id, sessionId, waiting.messageId, waiting.text, {
197329
198486
  result: waiting.decision === "allow" ? `Allowed by ${waiting.who} at ${waiting.sentAt}.` : `Denied by ${waiting.who} at ${waiting.sentAt}.`,
197330
198487
  shortReply: waiting.decision === "allow" ? "Approval granted." : "Approval denied."
@@ -197480,8 +198637,11 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
197480
198637
  return;
197481
198638
  }
197482
198639
  if (entry !== undefined && entry.expiresAt > this.now()) {
198640
+ if (approval.decision === "always" && entry.remember === undefined) {
198641
+ return;
198642
+ }
197483
198643
  this.dropApproval(id);
197484
- const event = { updateId: callback.updateId, approvalId: id, decision: approval.decision };
198644
+ const event = { updateId: callback.updateId, approvalId: id, decision: approval.decision, fromId: callback.fromId };
197485
198645
  const told = stream?.write(encodeSseEvent("approval", event, callback.updateId)) === true;
197486
198646
  const messageId = entry.messageId ?? callback.messageId;
197487
198647
  const when = clockText(this.now());
@@ -197499,6 +198659,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
197499
198659
  sentAt: when,
197500
198660
  ...messageId === undefined ? {} : { messageId },
197501
198661
  ...entry.text === undefined ? {} : { text: entry.text },
198662
+ ...approval.decision === "always" && entry.remember !== undefined ? { remember: entry.remember } : {},
197502
198663
  timer: this.timers.setTimeout(() => this.giveUpOnAck(id), this.approvalAckMs)
197503
198664
  });
197504
198665
  return;
@@ -197754,6 +198915,10 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
197754
198915
  }
197755
198916
  }
197756
198917
  }
198918
+ function alwaysButtonText(pattern) {
198919
+ const text = `Always: ${pattern.replace(/\s+/g, " ")}`;
198920
+ return text.length <= MAX_BUTTON_TEXT_CHARS ? text : `${text.slice(0, MAX_BUTTON_TEXT_CHARS - 1)}\u2026`;
198921
+ }
197757
198922
  function asCodeBlock(text) {
197758
198923
  const safe = (text.trim().length === 0 ? "(empty)" : text).replace(/\r\n?/g, `
197759
198924
  `).replace(/`{9,}/g, (run) => run.match(/`{1,8}/g).join("\u200B"));
@@ -198903,11 +200068,24 @@ function runStatus7(args) {
198903
200068
  };
198904
200069
  const credentialState = credential.status === "ok" ? "present" : credential.status;
198905
200070
  const fingerprint = credential.status === "ok" ? credentialFingerprint(credential.record) : undefined;
200071
+ const telegram = loadPosture();
200072
+ const remoteConfig = telegram === undefined ? undefined : loadRemoteConfig();
200073
+ const allowedUsers = remoteConfig?.ok === true ? remoteConfig.value.allowedUserIds.length : undefined;
198906
200074
  if (asJson) {
198907
200075
  console.log(JSON.stringify({
198908
200076
  ...report,
198909
200077
  credential: credentialState,
198910
200078
  ...fingerprint === undefined ? {} : { credentialFingerprint: fingerprint },
200079
+ ...telegram === undefined ? {} : {
200080
+ telegramPermissions: {
200081
+ defaultMode: telegram.defaultMode,
200082
+ runTimeoutMs: telegram.runTimeoutMs,
200083
+ approvalTimeoutMs: telegram.approvalTimeoutMs,
200084
+ savedRules: telegram.savedRules,
200085
+ ...allowedUsers !== undefined ? { allowedUsers } : {},
200086
+ shellModeOverridesDefault: true
200087
+ }
200088
+ },
198911
200089
  warnings: [...warnings].sort()
198912
200090
  }, null, 2));
198913
200091
  return;
@@ -198925,6 +200103,10 @@ function runStatus7(args) {
198925
200103
  }
198926
200104
  console.log(` credential: ${credentialState}${fingerprint === undefined ? "" : ` (fingerprint ${fingerprint})`}`);
198927
200105
  console.log(` pending approvals: ${report.pendingApprovals}`);
200106
+ if (telegram !== undefined) {
200107
+ console.log(` telegram: default mode ${telegram.defaultMode}, run limit ${formatPolicyDuration(telegram.runTimeoutMs)}, approval wait ${formatPolicyDuration(telegram.approvalTimeoutMs)}, ${telegram.savedRules} saved shell rule${telegram.savedRules === 1 ? "" : "s"}${allowedUsers !== undefined ? `, ${allowedUsers} allowed user${allowedUsers === 1 ? "" : "s"}` : ""}`);
200108
+ console.log(" a running shell's /mode overrides the default mode; /remote-policy changes the defaults");
200109
+ }
198928
200110
  if (report.message !== undefined) {
198929
200111
  console.log("");
198930
200112
  console.log(` ${style.yellow(symbols.bullet)} ${sanitizeForDisplay(report.message)}`);
@@ -199182,6 +200364,8 @@ function printHelp23() {
199182
200364
  }
199183
200365
  var BIND_FLAGS, ACK_FLAG = "--acknowledge-non-loopback", FORCE_FLAG = "--force", NO_ACK_FLAG = "--no-acknowledge-non-loopback", ENABLE_FLAG = "--enable", DISABLE_FLAG = "--disable";
199184
200366
  var init_serve = __esm(() => {
200367
+ init_config11();
200368
+ init_remote_policy_command();
199185
200369
  init_project_registry();
199186
200370
  init_serve_config();
199187
200371
  init_serve_credential();
@@ -210895,6 +212079,7 @@ var init_group_subcommands = __esm(() => {
210895
212079
  ["external", ["on", "off", "status", "list"]],
210896
212080
  ["auth", ["list", "login", "logout", "status"]],
210897
212081
  ["approvals", ["list", "allow", "deny"]],
212082
+ ["permissions", ["list", "remove"]],
210898
212083
  ["version", ["check"]],
210899
212084
  ["gdgraph", ["build", "query", "find", "symbol", "symbols", "path", "affected", "repomap", "context", "assets"]],
210900
212085
  ["ctx", ["status", "diff", "rg", "read", "run", "show", "hook", "install-hook", "uninstall-hook"]],
@@ -211219,6 +212404,8 @@ var VERSION3, CLI_ROUTES, MCP_KNOWN_SUBCOMMANDS, USAGE_BODY = `Usage:
211219
212404
  keryx serve config init|set|show
211220
212405
  keryx approvals list [--all] [--json] | allow <id> | deny <id>
211221
212406
  Answer, from this machine, a call a remote turn is waiting on (once, that call only)
212407
+ keryx permissions list [--json] | remove <number|pattern>
212408
+ The saved shell rules an Always answer left behind; take one back
211222
212409
  keryx update [--skip-runtime] [--hooks]
211223
212410
  keryx dashboard build
211224
212411
  keryx dashboard open
@@ -211399,6 +212586,7 @@ Commands:
211399
212586
  projects Inspect the user-global registry of initialized projects
211400
212587
  serve Loopback-bound authenticated HTTP entry (off by default; read-only routes)
211401
212588
  approvals Pending remote approvals: list them, allow or deny one call, once (the local answer path of the serve entry)
212589
+ permissions The saved shell rules (what Always remembered): list them, remove one
211402
212590
  update Refresh managed service files without touching data artifacts
211403
212591
  dashboard Build or open the project admin dashboard
211404
212592
  dash Rebuild and open .metaproject/keryx-dashboard.html
@@ -211479,6 +212667,7 @@ var init_cli_registry = __esm(() => {
211479
212667
  init_projects();
211480
212668
  init_serve();
211481
212669
  init_approvals();
212670
+ init_permissions_command();
211482
212671
  init_update();
211483
212672
  init_dashboard();
211484
212673
  init_agents2();
@@ -211522,6 +212711,7 @@ var init_cli_registry = __esm(() => {
211522
212711
  auth: authCommand2,
211523
212712
  serve: serveCommand,
211524
212713
  approvals: approvalsCommand,
212714
+ permissions: permissionsCommand,
211525
212715
  update: updateCommand,
211526
212716
  dashboard: dashboardCommand,
211527
212717
  dash: (rest) => dashboardCommand(rest.length > 0 ? rest : ["open"]),