clisbot 0.1.19 → 0.1.21

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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/dist/main.js +309 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,8 +64,11 @@ If you want to try first without persisting the token yet, just remove `--persis
64
64
  Current auth note:
65
65
 
66
66
  - DMs currently start in pairing mode by default.
67
- - The config shape already includes `ownerClaimWindowMinutes`, but automatic first-owner claim from the first DM is not implemented in the runtime yet.
68
- - Today, if you want an owner or app admin, grant that principal explicitly with `clisbot auth add-user app --role owner --user <principal>` or `clisbot auth add-user app --role admin --user <principal>`.
67
+ - If no app owner is configured yet, the first DM user during the first `ownerClaimWindowMinutes` becomes app `owner` automatically and does not need pairing approval.
68
+ - Today, if you want an owner or app admin, grant that principal explicitly with the platform prefix plus the channel-native user id, for example `telegram:1276408333` or `slack:U123ABC456`.
69
+ - Example commands:
70
+ - `clisbot auth add-user app --role owner --user telegram:1276408333`
71
+ - `clisbot auth add-user app --role admin --user slack:U123ABC456`
69
72
  - `clisbot auth --help` now covers role scopes, permission sets, and add/remove flows for users and permissions.
70
73
  - App-level auth and owner-claim semantics in [Authorization And Roles](docs/user-guide/auth-and-roles.md) describe both the current runtime reality and the remaining target-model gaps.
71
74
 
package/dist/main.js CHANGED
@@ -54487,6 +54487,9 @@ function renderPairingSetupHelpLines(prefix = "", options = {}) {
54487
54487
  lines.push(`${prefix} - Approve the returned Slack code with: \`clisbot pairing approve slack <code>\``);
54488
54488
  }
54489
54489
  lines.push(`${prefix} - Configured app owner/admin principals bypass pairing in DMs.`);
54490
+ if (options.ownerConfigured === false) {
54491
+ lines.push(`${prefix} - If no owner is configured yet, the first DM user during the first ${options.ownerClaimWindowMinutes ?? 30} minutes becomes app owner automatically.`);
54492
+ }
54490
54493
  return lines;
54491
54494
  }
54492
54495
  function renderTmuxDebugHelpLines(prefix = "") {
@@ -61890,6 +61893,8 @@ function getExecutableNames(command) {
61890
61893
  // src/runners/tmux/client.ts
61891
61894
  var MAIN_WINDOW_NAME = "main";
61892
61895
  var TMUX_NOT_FOUND_CODE = "ENOENT";
61896
+ var TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS = 1000;
61897
+ var TMUX_SERVER_BOOTSTRAP_POLL_MS = 25;
61893
61898
  var TMUX_SERVER_DEFAULTS = [
61894
61899
  ["exit-empty", "off"],
61895
61900
  ["destroy-unattached", "off"]
@@ -61900,6 +61905,9 @@ class TmuxClient {
61900
61905
  constructor(socketPath) {
61901
61906
  this.socketPath = socketPath;
61902
61907
  }
61908
+ isServerUnavailableOutput(output) {
61909
+ return /no server running|No such file or directory|failed to connect to server|error connecting to/i.test(output);
61910
+ }
61903
61911
  async exec(args, options = {}) {
61904
61912
  if (!commandExists("tmux")) {
61905
61913
  throw new Error("tmux is not installed or not available on PATH. Install tmux and restart clisbot.");
@@ -61951,7 +61959,10 @@ class TmuxClient {
61951
61959
  }
61952
61960
  const output = `${result.stderr}
61953
61961
  ${result.stdout}`.trim();
61954
- return !output.includes("no server running");
61962
+ if (this.isServerUnavailableOutput(output)) {
61963
+ return false;
61964
+ }
61965
+ return false;
61955
61966
  }
61956
61967
  async ensureServerDefaults() {
61957
61968
  if (!await this.isServerRunning()) {
@@ -61961,6 +61972,35 @@ ${result.stdout}`.trim();
61961
61972
  await this.execOrThrow(["set-option", "-g", name, value]);
61962
61973
  }
61963
61974
  }
61975
+ isBootstrapRetryableError(error) {
61976
+ const message = error instanceof Error ? error.message : String(error);
61977
+ return this.isServerUnavailableOutput(message);
61978
+ }
61979
+ async withServerBootstrapRetry(task) {
61980
+ const deadline = Date.now() + TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS;
61981
+ while (true) {
61982
+ try {
61983
+ return await task();
61984
+ } catch (error) {
61985
+ if (!this.isBootstrapRetryableError(error) || Date.now() >= deadline) {
61986
+ throw error;
61987
+ }
61988
+ await sleep(TMUX_SERVER_BOOTSTRAP_POLL_MS);
61989
+ }
61990
+ }
61991
+ }
61992
+ async waitForSessionBootstrap(sessionName) {
61993
+ const deadline = Date.now() + TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS;
61994
+ while (true) {
61995
+ if (await this.hasSession(sessionName)) {
61996
+ return;
61997
+ }
61998
+ if (Date.now() >= deadline) {
61999
+ throw new Error(`tmux session "${sessionName}" did not become reachable on socket ${this.socketPath} within ${TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS}ms.`);
62000
+ }
62001
+ await sleep(TMUX_SERVER_BOOTSTRAP_POLL_MS);
62002
+ }
62003
+ }
61964
62004
  async newSession(params) {
61965
62005
  await this.execOrThrow([
61966
62006
  "new-session",
@@ -61973,8 +62013,13 @@ ${result.stdout}`.trim();
61973
62013
  params.cwd,
61974
62014
  params.command
61975
62015
  ]);
61976
- await this.ensureServerDefaults();
61977
- await this.freezeWindowName(`${params.sessionName}:${MAIN_WINDOW_NAME}`);
62016
+ await this.waitForSessionBootstrap(params.sessionName);
62017
+ await this.withServerBootstrapRetry(async () => {
62018
+ await this.ensureServerDefaults();
62019
+ });
62020
+ await this.withServerBootstrapRetry(async () => {
62021
+ await this.freezeWindowName(`${params.sessionName}:${MAIN_WINDOW_NAME}`);
62022
+ });
61978
62023
  }
61979
62024
  async newWindow(params) {
61980
62025
  const paneId = await this.execOrThrow([
@@ -66481,6 +66526,7 @@ async function monitorTmuxRun(params) {
66481
66526
  promptSubmitDelayMs: params.promptSubmitDelayMs,
66482
66527
  timingContext: params.timingContext
66483
66528
  });
66529
+ await params.onPromptSubmitted?.();
66484
66530
  logLatencyDebug("tmux-submit-complete", params.timingContext, {
66485
66531
  sessionName: params.sessionName,
66486
66532
  promptSubmitDelayMs: params.promptSubmitDelayMs,
@@ -66655,7 +66701,8 @@ class ActiveRunManager {
66655
66701
  observers: new Map,
66656
66702
  observerFailures: new Map,
66657
66703
  initialResult,
66658
- latestUpdate: update
66704
+ latestUpdate: update,
66705
+ steeringReady: true
66659
66706
  });
66660
66707
  this.startRunMonitor(resolved.sessionKey, {
66661
66708
  prompt: undefined,
@@ -66700,7 +66747,8 @@ class ActiveRunManager {
66700
66747
  fullSnapshot: "",
66701
66748
  initialSnapshot: "",
66702
66749
  note: "Starting runner session..."
66703
- })
66750
+ }),
66751
+ steeringReady: false
66704
66752
  });
66705
66753
  try {
66706
66754
  const { resolved, initialSnapshot } = await this.runnerSessions.preparePromptSession(target, {
@@ -66794,6 +66842,9 @@ class ActiveRunManager {
66794
66842
  hasActiveRun(target) {
66795
66843
  return this.activeRuns.has(target.sessionKey);
66796
66844
  }
66845
+ canSteerActiveRun(target) {
66846
+ return this.activeRuns.get(target.sessionKey)?.steeringReady ?? false;
66847
+ }
66797
66848
  async stop() {
66798
66849
  this.stopping = true;
66799
66850
  const activeRuns = [...this.activeRuns.values()];
@@ -66898,6 +66949,9 @@ class ActiveRunManager {
66898
66949
  }
66899
66950
  (async () => {
66900
66951
  try {
66952
+ if (!params.prompt) {
66953
+ run.steeringReady = true;
66954
+ }
66901
66955
  await monitorTmuxRun({
66902
66956
  tmux: this.tmux,
66903
66957
  sessionName: run.resolved.sessionName,
@@ -66912,6 +66966,13 @@ class ActiveRunManager {
66912
66966
  initialSnapshot: params.initialSnapshot,
66913
66967
  detachedAlready: params.detachedAlready,
66914
66968
  timingContext: params.timingContext,
66969
+ onPromptSubmitted: async () => {
66970
+ const currentRun = this.activeRuns.get(sessionKey);
66971
+ if (!currentRun) {
66972
+ return;
66973
+ }
66974
+ currentRun.steeringReady = true;
66975
+ },
66915
66976
  onRunning: async (update) => {
66916
66977
  const currentRun = this.activeRuns.get(sessionKey);
66917
66978
  if (!currentRun) {
@@ -67094,6 +67155,9 @@ class AgentService {
67094
67155
  hasActiveRun(target) {
67095
67156
  return this.activeRuns.hasActiveRun(target);
67096
67157
  }
67158
+ canSteerActiveRun(target) {
67159
+ return this.activeRuns.canSteerActiveRun(target);
67160
+ }
67097
67161
  async submitSessionInput(target, text) {
67098
67162
  return this.runnerSessions.submitSessionInput(target, text);
67099
67163
  }
@@ -68207,6 +68271,28 @@ function renderTranscriptDisabledMessage() {
68207
68271
  ].join(`
68208
68272
  `);
68209
68273
  }
68274
+ function renderStartupSteeringUnavailableMessage() {
68275
+ return [
68276
+ "The active run is still starting and cannot accept steering input yet.",
68277
+ "Send a normal follow-up message to keep it ordered behind the first prompt, or wait until startup finishes before using `/steer`."
68278
+ ].join(`
68279
+ `);
68280
+ }
68281
+ function renderPrincipalFormat(identity) {
68282
+ if (identity.platform === "slack") {
68283
+ return "slack:<nativeUserId>";
68284
+ }
68285
+ return "telegram:<nativeUserId>";
68286
+ }
68287
+ function renderPrincipalExample(identity) {
68288
+ if (identity.senderId) {
68289
+ return `${identity.platform}:${identity.senderId}`;
68290
+ }
68291
+ if (identity.platform === "slack") {
68292
+ return "slack:U123ABC456";
68293
+ }
68294
+ return "telegram:1276408333";
68295
+ }
68210
68296
  function renderWhoAmIMessage(params) {
68211
68297
  const lines = [
68212
68298
  "Who am I",
@@ -68231,7 +68317,7 @@ function renderWhoAmIMessage(params) {
68231
68317
  if (params.identity.topicId) {
68232
68318
  lines.push(`topicId: \`${params.identity.topicId}\``);
68233
68319
  }
68234
- lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
68320
+ lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
68235
68321
  return lines.join(`
68236
68322
  `);
68237
68323
  }
@@ -68259,7 +68345,7 @@ function renderRouteStatusMessage(params) {
68259
68345
  if (params.identity.topicId) {
68260
68346
  lines.push(`topicId: \`${params.identity.topicId}\``);
68261
68347
  }
68262
- lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
68348
+ lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
68263
68349
  if (params.runtimeState.startedAt) {
68264
68350
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
68265
68351
  }
@@ -68921,6 +69007,7 @@ async function processChannelInteraction(params) {
68921
69007
  const explicitQueueMessage = slashCommand?.type === "queue" ? slashCommand.text.trim() : undefined;
68922
69008
  const explicitSteerMessage = slashCommand?.type === "steer" ? slashCommand.text.trim() : undefined;
68923
69009
  const sessionBusy = await (params.agentService.isAwaitingFollowUpRouting?.(params.sessionTarget) ?? params.agentService.isSessionBusy?.(params.sessionTarget) ?? false);
69010
+ const canSteerActiveRun = params.agentService.canSteerActiveRun?.(params.sessionTarget) ?? !sessionBusy;
68924
69011
  const queueByMode = !explicitQueueMessage && params.route.additionalMessageMode === "queue" && sessionBusy;
68925
69012
  const forceQueuedDelivery = typeof explicitQueueMessage === "string" || queueByMode;
68926
69013
  const delayedPromptText = explicitQueueMessage ? params.agentPromptBuilder ? params.agentPromptBuilder(explicitQueueMessage) : explicitQueueMessage : params.agentPromptText ?? params.text;
@@ -69342,6 +69429,11 @@ ${escapeCodeFence(shellResult.output)}
69342
69429
  await params.agentService.recordConversationReply(params.sessionTarget);
69343
69430
  return interactionResult;
69344
69431
  }
69432
+ if (!canSteerActiveRun) {
69433
+ await params.postText(renderStartupSteeringUnavailableMessage());
69434
+ await params.agentService.recordConversationReply(params.sessionTarget);
69435
+ return interactionResult;
69436
+ }
69345
69437
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage, params.protectedControlMutationRule));
69346
69438
  await params.postText("Steered.");
69347
69439
  await params.agentService.recordConversationReply(params.sessionTarget);
@@ -69350,7 +69442,7 @@ ${escapeCodeFence(shellResult.output)}
69350
69442
  };
69351
69443
  }
69352
69444
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
69353
- if (sessionBusy) {
69445
+ if (sessionBusy && canSteerActiveRun) {
69354
69446
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text, params.protectedControlMutationRule));
69355
69447
  return {
69356
69448
  processingIndicatorLifecycle: "active-run"
@@ -69443,7 +69535,7 @@ function mergeRoleRecord2(defaults, overrides) {
69443
69535
  }
69444
69536
  return merged;
69445
69537
  }
69446
- function normalizePrincipal(principal) {
69538
+ function normalizeAuthPrincipal(principal) {
69447
69539
  const trimmed = principal.trim();
69448
69540
  if (!trimmed) {
69449
69541
  return "";
@@ -69461,17 +69553,17 @@ function normalizePrincipal(principal) {
69461
69553
  return `${platform}:${userId.trim()}`;
69462
69554
  }
69463
69555
  function normalizeRoleUsers(users) {
69464
- return (users ?? []).map(normalizePrincipal).filter(Boolean);
69556
+ return (users ?? []).map(normalizeAuthPrincipal).filter(Boolean);
69465
69557
  }
69466
- function resolvePrincipal(identity) {
69558
+ function resolveAuthPrincipal(identity) {
69467
69559
  const senderId = identity.senderId?.trim();
69468
69560
  if (!senderId) {
69469
69561
  return;
69470
69562
  }
69471
69563
  if (identity.platform === "slack") {
69472
- return normalizePrincipal(`slack:${senderId}`);
69564
+ return normalizeAuthPrincipal(`slack:${senderId}`);
69473
69565
  }
69474
- return normalizePrincipal(`telegram:${senderId}`);
69566
+ return normalizeAuthPrincipal(`telegram:${senderId}`);
69475
69567
  }
69476
69568
  function findExplicitRole(roles, principal) {
69477
69569
  if (!principal || !roles) {
@@ -69503,7 +69595,7 @@ function hasAppPermission(config, appRole, permission) {
69503
69595
  return getAllowedPermissions(config.app.auth.roles, appRole).has(permission);
69504
69596
  }
69505
69597
  function resolveChannelAuth(params) {
69506
- const principal = resolvePrincipal(params.identity);
69598
+ const principal = resolveAuthPrincipal(params.identity);
69507
69599
  const appAuth = params.config.app.auth;
69508
69600
  const explicitAppRole = findExplicitRole(appAuth.roles, principal);
69509
69601
  const appRole = explicitAppRole ?? appAuth.defaultRole;
@@ -69523,6 +69615,128 @@ function resolveChannelAuth(params) {
69523
69615
  };
69524
69616
  }
69525
69617
 
69618
+ // src/auth/owner-claim.ts
69619
+ var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
69620
+ var OWNER_CLAIM_RUNTIME_STARTED_AT_MS = Date.now();
69621
+ var CONFIG_LOCK_OPTIONS = {
69622
+ retries: {
69623
+ retries: 10,
69624
+ factor: 2,
69625
+ minTimeout: 50,
69626
+ maxTimeout: 2000,
69627
+ randomize: true
69628
+ },
69629
+ stale: 30000
69630
+ };
69631
+ var ownerClaimRuntimeState = {
69632
+ initialized: false,
69633
+ armed: false,
69634
+ closed: false,
69635
+ openedAtMs: OWNER_CLAIM_RUNTIME_STARTED_AT_MS,
69636
+ windowMs: 0
69637
+ };
69638
+ function getOwnerUsers(config) {
69639
+ return config.app.auth.roles.owner?.users ?? [];
69640
+ }
69641
+ function hasConfiguredOwner(config) {
69642
+ return getOwnerUsers(config).some((entry) => entry.trim().length > 0);
69643
+ }
69644
+ function syncRuntimeStateWithConfig(config) {
69645
+ if (hasConfiguredOwner(config)) {
69646
+ ownerClaimRuntimeState.closed = true;
69647
+ }
69648
+ }
69649
+ function primeOwnerClaimRuntime(config, nowMs = OWNER_CLAIM_RUNTIME_STARTED_AT_MS) {
69650
+ if (!ownerClaimRuntimeState.initialized) {
69651
+ ownerClaimRuntimeState.initialized = true;
69652
+ ownerClaimRuntimeState.armed = !hasConfiguredOwner(config);
69653
+ ownerClaimRuntimeState.closed = !ownerClaimRuntimeState.armed;
69654
+ ownerClaimRuntimeState.openedAtMs = nowMs;
69655
+ ownerClaimRuntimeState.windowMs = Math.max(0, config.app.auth.ownerClaimWindowMinutes * 60000);
69656
+ }
69657
+ syncRuntimeStateWithConfig(config);
69658
+ return { ...ownerClaimRuntimeState };
69659
+ }
69660
+ function isOwnerClaimOpen(config, nowMs = Date.now()) {
69661
+ primeOwnerClaimRuntime(config);
69662
+ syncRuntimeStateWithConfig(config);
69663
+ if (ownerClaimRuntimeState.closed || !ownerClaimRuntimeState.armed) {
69664
+ return false;
69665
+ }
69666
+ return nowMs - ownerClaimRuntimeState.openedAtMs <= ownerClaimRuntimeState.windowMs;
69667
+ }
69668
+ function syncOwnerUsers(target, source) {
69669
+ target.app.auth.roles.owner = {
69670
+ ...target.app.auth.roles.owner,
69671
+ allow: [...source.app.auth.roles.owner?.allow ?? target.app.auth.roles.owner?.allow ?? []],
69672
+ users: [...source.app.auth.roles.owner?.users ?? []]
69673
+ };
69674
+ }
69675
+ async function withConfigLock(configPath, fn) {
69676
+ const expandedPath = await ensureEditableConfigFile(configPath);
69677
+ let release;
69678
+ try {
69679
+ release = await import_proper_lockfile2.default.lock(expandedPath, CONFIG_LOCK_OPTIONS);
69680
+ return await fn(expandedPath);
69681
+ } finally {
69682
+ if (release) {
69683
+ try {
69684
+ await release();
69685
+ } catch {}
69686
+ }
69687
+ }
69688
+ }
69689
+ async function claimFirstOwnerFromDirectMessage(params) {
69690
+ const nowMs = params.nowMs ?? Date.now();
69691
+ primeOwnerClaimRuntime(params.config);
69692
+ if (params.identity.conversationKind !== "dm") {
69693
+ return { claimed: false, principal: undefined };
69694
+ }
69695
+ const principal = resolveAuthPrincipal(params.identity);
69696
+ if (!principal || !isOwnerClaimOpen(params.config, nowMs)) {
69697
+ return { claimed: false, principal };
69698
+ }
69699
+ const result = await withConfigLock(params.configPath, async (expandedPath) => {
69700
+ const { config: freshConfig } = await readEditableConfig(expandedPath);
69701
+ primeOwnerClaimRuntime(freshConfig);
69702
+ syncRuntimeStateWithConfig(freshConfig);
69703
+ if (!isOwnerClaimOpen(freshConfig, nowMs)) {
69704
+ syncOwnerUsers(params.config, freshConfig);
69705
+ return { claimed: false, principal, configPath: expandedPath };
69706
+ }
69707
+ const currentOwners = getOwnerUsers(freshConfig).map((entry) => entry.trim()).filter(Boolean);
69708
+ if (currentOwners.includes(principal)) {
69709
+ syncOwnerUsers(params.config, freshConfig);
69710
+ ownerClaimRuntimeState.closed = true;
69711
+ return { claimed: false, principal, configPath: expandedPath };
69712
+ }
69713
+ freshConfig.app.auth.roles.owner.users = [...currentOwners, principal];
69714
+ await writeEditableConfig(expandedPath, freshConfig);
69715
+ syncOwnerUsers(params.config, freshConfig);
69716
+ ownerClaimRuntimeState.closed = true;
69717
+ console.log(`clisbot auto-claimed first owner ${principal}`);
69718
+ return { claimed: true, principal, configPath: expandedPath };
69719
+ });
69720
+ return result;
69721
+ }
69722
+ function renderFirstOwnerClaimMessage(params) {
69723
+ return [
69724
+ "First owner claim complete.",
69725
+ "",
69726
+ `principal: \`${params.principal}\``,
69727
+ "role: `owner`",
69728
+ `reason: no owner was configured, and this was the first direct message received during the first ${params.ownerClaimWindowMinutes} minutes after runtime start`,
69729
+ "pairing: not required for you anymore because app owners bypass DM pairing",
69730
+ "",
69731
+ "You can now:",
69732
+ "- chat without pairing approval",
69733
+ "- use full app-level control",
69734
+ "- manage auth, channels, and agent settings",
69735
+ "- use admin-level actions across all agents and routed surfaces"
69736
+ ].join(`
69737
+ `);
69738
+ }
69739
+
69526
69740
  // src/channels/slack/session-routing.ts
69527
69741
  function resolveSlackConversationTarget(params) {
69528
69742
  const sessionConfig = params.loadedConfig.raw.session;
@@ -70731,21 +70945,48 @@ class SlackSocketService {
70731
70945
  if (params.conversationKind === "dm") {
70732
70946
  const directUserId = typeof event.user === "string" ? event.user.trim() : "";
70733
70947
  const dmConfig = this.loadedConfig.raw.channels.slack.directMessages;
70734
- const auth2 = resolveChannelAuth({
70735
- config: this.loadedConfig.raw,
70736
- agentId: params.route.agentId,
70737
- identity: {
70738
- platform: "slack",
70739
- conversationKind: params.conversationKind,
70740
- senderId: directUserId || undefined,
70741
- channelId
70742
- }
70743
- });
70948
+ const dmIdentity = {
70949
+ platform: "slack",
70950
+ conversationKind: params.conversationKind,
70951
+ senderId: directUserId || undefined,
70952
+ channelId
70953
+ };
70744
70954
  if (!directUserId || dmConfig.policy === "disabled") {
70745
70955
  debugSlackEvent("drop-dm-disabled", { eventId, directUserId });
70746
70956
  await this.processedEventsStore.markCompleted(eventId);
70747
70957
  return;
70748
70958
  }
70959
+ let ownerClaimed = false;
70960
+ let ownerPrincipal;
70961
+ try {
70962
+ const claimResult = await claimFirstOwnerFromDirectMessage({
70963
+ config: this.loadedConfig.raw,
70964
+ configPath: this.loadedConfig.configPath,
70965
+ identity: dmIdentity
70966
+ });
70967
+ ownerClaimed = claimResult.claimed;
70968
+ ownerPrincipal = claimResult.principal;
70969
+ } catch (error) {
70970
+ console.error("slack first-owner claim failed", error);
70971
+ }
70972
+ if (ownerClaimed && ownerPrincipal) {
70973
+ try {
70974
+ await postSlackText(this.app.client, {
70975
+ channel: channelId,
70976
+ text: renderFirstOwnerClaimMessage({
70977
+ principal: ownerPrincipal,
70978
+ ownerClaimWindowMinutes: this.loadedConfig.raw.app.auth.ownerClaimWindowMinutes
70979
+ })
70980
+ });
70981
+ } catch (error) {
70982
+ console.error("slack first-owner claim reply failed", error);
70983
+ }
70984
+ }
70985
+ const auth2 = resolveChannelAuth({
70986
+ config: this.loadedConfig.raw,
70987
+ agentId: params.route.agentId,
70988
+ identity: dmIdentity
70989
+ });
70749
70990
  if (dmConfig.policy !== "open" && !auth2.mayBypassPairing) {
70750
70991
  const storedAllowFrom = await readChannelAllowFromStore("slack");
70751
70992
  const allowed = isSlackSenderAllowed({
@@ -72256,20 +72497,47 @@ class TelegramPollingService {
72256
72497
  const directMessages = this.loadedConfig.raw.channels.telegram.directMessages;
72257
72498
  const senderId = message.from?.id != null ? String(message.from.id) : "";
72258
72499
  const senderUsername = message.from?.username;
72259
- const auth = resolveChannelAuth({
72260
- config: this.loadedConfig.raw,
72261
- agentId: routeInfo.route.agentId,
72262
- identity: {
72263
- platform: "telegram",
72264
- conversationKind: routeInfo.conversationKind,
72265
- senderId: senderId || undefined,
72266
- chatId: String(message.chat.id)
72267
- }
72268
- });
72500
+ const dmIdentity = {
72501
+ platform: "telegram",
72502
+ conversationKind: routeInfo.conversationKind,
72503
+ senderId: senderId || undefined,
72504
+ chatId: String(message.chat.id)
72505
+ };
72269
72506
  if (!senderId || directMessages.policy === "disabled") {
72270
72507
  await this.processedEventsStore.markCompleted(eventId);
72271
72508
  return;
72272
72509
  }
72510
+ let ownerClaimed = false;
72511
+ let ownerPrincipal;
72512
+ try {
72513
+ const claimResult = await claimFirstOwnerFromDirectMessage({
72514
+ config: this.loadedConfig.raw,
72515
+ configPath: this.loadedConfig.configPath,
72516
+ identity: dmIdentity
72517
+ });
72518
+ ownerClaimed = claimResult.claimed;
72519
+ ownerPrincipal = claimResult.principal;
72520
+ } catch (error) {
72521
+ console.error("telegram first-owner claim failed", error);
72522
+ }
72523
+ if (ownerClaimed && ownerPrincipal) {
72524
+ try {
72525
+ await callTelegramApi(this.accountConfig.botToken, "sendMessage", {
72526
+ chat_id: message.chat.id,
72527
+ text: renderFirstOwnerClaimMessage({
72528
+ principal: ownerPrincipal,
72529
+ ownerClaimWindowMinutes: this.loadedConfig.raw.app.auth.ownerClaimWindowMinutes
72530
+ })
72531
+ });
72532
+ } catch (error) {
72533
+ console.error("telegram first-owner claim reply failed", error);
72534
+ }
72535
+ }
72536
+ const auth = resolveChannelAuth({
72537
+ config: this.loadedConfig.raw,
72538
+ agentId: routeInfo.route.agentId,
72539
+ identity: dmIdentity
72540
+ });
72273
72541
  if (directMessages.policy !== "open" && !auth.mayBypassPairing) {
72274
72542
  const storedAllowFrom = await readChannelAllowFromStore("telegram");
72275
72543
  const allowed = isTelegramSenderAllowed({
@@ -73293,6 +73561,7 @@ class RuntimeSupervisor {
73293
73561
  }
73294
73562
  async createRuntime(loadedConfig) {
73295
73563
  const runtimeId = this.nextRuntimeId++;
73564
+ primeOwnerClaimRuntime(loadedConfig.raw);
73296
73565
  const agentService = this.dependencies.createAgentService(loadedConfig);
73297
73566
  const processedEventsStore = this.dependencies.createProcessedEventsStore(loadedConfig.processedEventsPath);
73298
73567
  const activityStore = this.dependencies.createActivityStore();
@@ -73753,7 +74022,8 @@ function appendAuthOnboardingLines(lines, summary, prefix = "") {
73753
74022
  lines.push(`${prefix}Auth onboarding:`);
73754
74023
  if (!hasConfiguredPrivilegedPrincipal(summary)) {
73755
74024
  lines.push(`${prefix} - get the principal from a surface the bot can already see; Telegram groups or topics can use \`/whoami\` before routing, while DMs with pairing must pair first`);
73756
- lines.push(`${prefix} - set the first owner with: \`clisbot auth add-user app --role owner --user <principal>\``);
74025
+ lines.push(`${prefix} - if no owner exists yet, the first DM user during the first ${summary.ownerSummary.ownerClaimWindowMinutes} minutes becomes app owner automatically`);
74026
+ lines.push(`${prefix} - after the first owner exists, add more principals with: \`clisbot auth add-user app --role <owner|admin> --user <principal>\``);
73757
74027
  } else {
73758
74028
  lines.push(`${prefix} - inspect current app roles with: \`clisbot auth show app\``);
73759
74029
  }
@@ -73874,6 +74144,8 @@ function renderStartSummary(summary) {
73874
74144
  lines.push(...renderPairingSetupHelpLines(" ", {
73875
74145
  slackEnabled: summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled),
73876
74146
  telegramEnabled: summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled),
74147
+ ownerConfigured: hasConfiguredPrivilegedPrincipal(summary),
74148
+ ownerClaimWindowMinutes: summary.ownerSummary.ownerClaimWindowMinutes,
73877
74149
  slackDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "slack")?.directMessagesPolicy,
73878
74150
  telegramDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "telegram")?.directMessagesPolicy,
73879
74151
  conditionalOnly: true
@@ -73896,6 +74168,8 @@ function renderStartSummary(summary) {
73896
74168
  lines.push(...renderPairingSetupHelpLines("", {
73897
74169
  slackEnabled: summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled),
73898
74170
  telegramEnabled: summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled),
74171
+ ownerConfigured: hasConfiguredPrivilegedPrincipal(summary),
74172
+ ownerClaimWindowMinutes: summary.ownerSummary.ownerClaimWindowMinutes,
73899
74173
  slackDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "slack")?.directMessagesPolicy,
73900
74174
  telegramDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "telegram")?.directMessagesPolicy,
73901
74175
  conditionalOnly: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",