clisbot 0.1.20 → 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 +264 -32
  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 = "") {
@@ -66523,6 +66526,7 @@ async function monitorTmuxRun(params) {
66523
66526
  promptSubmitDelayMs: params.promptSubmitDelayMs,
66524
66527
  timingContext: params.timingContext
66525
66528
  });
66529
+ await params.onPromptSubmitted?.();
66526
66530
  logLatencyDebug("tmux-submit-complete", params.timingContext, {
66527
66531
  sessionName: params.sessionName,
66528
66532
  promptSubmitDelayMs: params.promptSubmitDelayMs,
@@ -66697,7 +66701,8 @@ class ActiveRunManager {
66697
66701
  observers: new Map,
66698
66702
  observerFailures: new Map,
66699
66703
  initialResult,
66700
- latestUpdate: update
66704
+ latestUpdate: update,
66705
+ steeringReady: true
66701
66706
  });
66702
66707
  this.startRunMonitor(resolved.sessionKey, {
66703
66708
  prompt: undefined,
@@ -66742,7 +66747,8 @@ class ActiveRunManager {
66742
66747
  fullSnapshot: "",
66743
66748
  initialSnapshot: "",
66744
66749
  note: "Starting runner session..."
66745
- })
66750
+ }),
66751
+ steeringReady: false
66746
66752
  });
66747
66753
  try {
66748
66754
  const { resolved, initialSnapshot } = await this.runnerSessions.preparePromptSession(target, {
@@ -66836,6 +66842,9 @@ class ActiveRunManager {
66836
66842
  hasActiveRun(target) {
66837
66843
  return this.activeRuns.has(target.sessionKey);
66838
66844
  }
66845
+ canSteerActiveRun(target) {
66846
+ return this.activeRuns.get(target.sessionKey)?.steeringReady ?? false;
66847
+ }
66839
66848
  async stop() {
66840
66849
  this.stopping = true;
66841
66850
  const activeRuns = [...this.activeRuns.values()];
@@ -66940,6 +66949,9 @@ class ActiveRunManager {
66940
66949
  }
66941
66950
  (async () => {
66942
66951
  try {
66952
+ if (!params.prompt) {
66953
+ run.steeringReady = true;
66954
+ }
66943
66955
  await monitorTmuxRun({
66944
66956
  tmux: this.tmux,
66945
66957
  sessionName: run.resolved.sessionName,
@@ -66954,6 +66966,13 @@ class ActiveRunManager {
66954
66966
  initialSnapshot: params.initialSnapshot,
66955
66967
  detachedAlready: params.detachedAlready,
66956
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
+ },
66957
66976
  onRunning: async (update) => {
66958
66977
  const currentRun = this.activeRuns.get(sessionKey);
66959
66978
  if (!currentRun) {
@@ -67136,6 +67155,9 @@ class AgentService {
67136
67155
  hasActiveRun(target) {
67137
67156
  return this.activeRuns.hasActiveRun(target);
67138
67157
  }
67158
+ canSteerActiveRun(target) {
67159
+ return this.activeRuns.canSteerActiveRun(target);
67160
+ }
67139
67161
  async submitSessionInput(target, text) {
67140
67162
  return this.runnerSessions.submitSessionInput(target, text);
67141
67163
  }
@@ -68249,6 +68271,28 @@ function renderTranscriptDisabledMessage() {
68249
68271
  ].join(`
68250
68272
  `);
68251
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
+ }
68252
68296
  function renderWhoAmIMessage(params) {
68253
68297
  const lines = [
68254
68298
  "Who am I",
@@ -68273,7 +68317,7 @@ function renderWhoAmIMessage(params) {
68273
68317
  if (params.identity.topicId) {
68274
68318
  lines.push(`topicId: \`${params.identity.topicId}\``);
68275
68319
  }
68276
- 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}\``);
68277
68321
  return lines.join(`
68278
68322
  `);
68279
68323
  }
@@ -68301,7 +68345,7 @@ function renderRouteStatusMessage(params) {
68301
68345
  if (params.identity.topicId) {
68302
68346
  lines.push(`topicId: \`${params.identity.topicId}\``);
68303
68347
  }
68304
- 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}\``);
68305
68349
  if (params.runtimeState.startedAt) {
68306
68350
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
68307
68351
  }
@@ -68963,6 +69007,7 @@ async function processChannelInteraction(params) {
68963
69007
  const explicitQueueMessage = slashCommand?.type === "queue" ? slashCommand.text.trim() : undefined;
68964
69008
  const explicitSteerMessage = slashCommand?.type === "steer" ? slashCommand.text.trim() : undefined;
68965
69009
  const sessionBusy = await (params.agentService.isAwaitingFollowUpRouting?.(params.sessionTarget) ?? params.agentService.isSessionBusy?.(params.sessionTarget) ?? false);
69010
+ const canSteerActiveRun = params.agentService.canSteerActiveRun?.(params.sessionTarget) ?? !sessionBusy;
68966
69011
  const queueByMode = !explicitQueueMessage && params.route.additionalMessageMode === "queue" && sessionBusy;
68967
69012
  const forceQueuedDelivery = typeof explicitQueueMessage === "string" || queueByMode;
68968
69013
  const delayedPromptText = explicitQueueMessage ? params.agentPromptBuilder ? params.agentPromptBuilder(explicitQueueMessage) : explicitQueueMessage : params.agentPromptText ?? params.text;
@@ -69384,6 +69429,11 @@ ${escapeCodeFence(shellResult.output)}
69384
69429
  await params.agentService.recordConversationReply(params.sessionTarget);
69385
69430
  return interactionResult;
69386
69431
  }
69432
+ if (!canSteerActiveRun) {
69433
+ await params.postText(renderStartupSteeringUnavailableMessage());
69434
+ await params.agentService.recordConversationReply(params.sessionTarget);
69435
+ return interactionResult;
69436
+ }
69387
69437
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage, params.protectedControlMutationRule));
69388
69438
  await params.postText("Steered.");
69389
69439
  await params.agentService.recordConversationReply(params.sessionTarget);
@@ -69392,7 +69442,7 @@ ${escapeCodeFence(shellResult.output)}
69392
69442
  };
69393
69443
  }
69394
69444
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
69395
- if (sessionBusy) {
69445
+ if (sessionBusy && canSteerActiveRun) {
69396
69446
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text, params.protectedControlMutationRule));
69397
69447
  return {
69398
69448
  processingIndicatorLifecycle: "active-run"
@@ -69485,7 +69535,7 @@ function mergeRoleRecord2(defaults, overrides) {
69485
69535
  }
69486
69536
  return merged;
69487
69537
  }
69488
- function normalizePrincipal(principal) {
69538
+ function normalizeAuthPrincipal(principal) {
69489
69539
  const trimmed = principal.trim();
69490
69540
  if (!trimmed) {
69491
69541
  return "";
@@ -69503,17 +69553,17 @@ function normalizePrincipal(principal) {
69503
69553
  return `${platform}:${userId.trim()}`;
69504
69554
  }
69505
69555
  function normalizeRoleUsers(users) {
69506
- return (users ?? []).map(normalizePrincipal).filter(Boolean);
69556
+ return (users ?? []).map(normalizeAuthPrincipal).filter(Boolean);
69507
69557
  }
69508
- function resolvePrincipal(identity) {
69558
+ function resolveAuthPrincipal(identity) {
69509
69559
  const senderId = identity.senderId?.trim();
69510
69560
  if (!senderId) {
69511
69561
  return;
69512
69562
  }
69513
69563
  if (identity.platform === "slack") {
69514
- return normalizePrincipal(`slack:${senderId}`);
69564
+ return normalizeAuthPrincipal(`slack:${senderId}`);
69515
69565
  }
69516
- return normalizePrincipal(`telegram:${senderId}`);
69566
+ return normalizeAuthPrincipal(`telegram:${senderId}`);
69517
69567
  }
69518
69568
  function findExplicitRole(roles, principal) {
69519
69569
  if (!principal || !roles) {
@@ -69545,7 +69595,7 @@ function hasAppPermission(config, appRole, permission) {
69545
69595
  return getAllowedPermissions(config.app.auth.roles, appRole).has(permission);
69546
69596
  }
69547
69597
  function resolveChannelAuth(params) {
69548
- const principal = resolvePrincipal(params.identity);
69598
+ const principal = resolveAuthPrincipal(params.identity);
69549
69599
  const appAuth = params.config.app.auth;
69550
69600
  const explicitAppRole = findExplicitRole(appAuth.roles, principal);
69551
69601
  const appRole = explicitAppRole ?? appAuth.defaultRole;
@@ -69565,6 +69615,128 @@ function resolveChannelAuth(params) {
69565
69615
  };
69566
69616
  }
69567
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
+
69568
69740
  // src/channels/slack/session-routing.ts
69569
69741
  function resolveSlackConversationTarget(params) {
69570
69742
  const sessionConfig = params.loadedConfig.raw.session;
@@ -70773,21 +70945,48 @@ class SlackSocketService {
70773
70945
  if (params.conversationKind === "dm") {
70774
70946
  const directUserId = typeof event.user === "string" ? event.user.trim() : "";
70775
70947
  const dmConfig = this.loadedConfig.raw.channels.slack.directMessages;
70776
- const auth2 = resolveChannelAuth({
70777
- config: this.loadedConfig.raw,
70778
- agentId: params.route.agentId,
70779
- identity: {
70780
- platform: "slack",
70781
- conversationKind: params.conversationKind,
70782
- senderId: directUserId || undefined,
70783
- channelId
70784
- }
70785
- });
70948
+ const dmIdentity = {
70949
+ platform: "slack",
70950
+ conversationKind: params.conversationKind,
70951
+ senderId: directUserId || undefined,
70952
+ channelId
70953
+ };
70786
70954
  if (!directUserId || dmConfig.policy === "disabled") {
70787
70955
  debugSlackEvent("drop-dm-disabled", { eventId, directUserId });
70788
70956
  await this.processedEventsStore.markCompleted(eventId);
70789
70957
  return;
70790
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
+ });
70791
70990
  if (dmConfig.policy !== "open" && !auth2.mayBypassPairing) {
70792
70991
  const storedAllowFrom = await readChannelAllowFromStore("slack");
70793
70992
  const allowed = isSlackSenderAllowed({
@@ -72298,20 +72497,47 @@ class TelegramPollingService {
72298
72497
  const directMessages = this.loadedConfig.raw.channels.telegram.directMessages;
72299
72498
  const senderId = message.from?.id != null ? String(message.from.id) : "";
72300
72499
  const senderUsername = message.from?.username;
72301
- const auth = resolveChannelAuth({
72302
- config: this.loadedConfig.raw,
72303
- agentId: routeInfo.route.agentId,
72304
- identity: {
72305
- platform: "telegram",
72306
- conversationKind: routeInfo.conversationKind,
72307
- senderId: senderId || undefined,
72308
- chatId: String(message.chat.id)
72309
- }
72310
- });
72500
+ const dmIdentity = {
72501
+ platform: "telegram",
72502
+ conversationKind: routeInfo.conversationKind,
72503
+ senderId: senderId || undefined,
72504
+ chatId: String(message.chat.id)
72505
+ };
72311
72506
  if (!senderId || directMessages.policy === "disabled") {
72312
72507
  await this.processedEventsStore.markCompleted(eventId);
72313
72508
  return;
72314
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
+ });
72315
72541
  if (directMessages.policy !== "open" && !auth.mayBypassPairing) {
72316
72542
  const storedAllowFrom = await readChannelAllowFromStore("telegram");
72317
72543
  const allowed = isTelegramSenderAllowed({
@@ -73335,6 +73561,7 @@ class RuntimeSupervisor {
73335
73561
  }
73336
73562
  async createRuntime(loadedConfig) {
73337
73563
  const runtimeId = this.nextRuntimeId++;
73564
+ primeOwnerClaimRuntime(loadedConfig.raw);
73338
73565
  const agentService = this.dependencies.createAgentService(loadedConfig);
73339
73566
  const processedEventsStore = this.dependencies.createProcessedEventsStore(loadedConfig.processedEventsPath);
73340
73567
  const activityStore = this.dependencies.createActivityStore();
@@ -73795,7 +74022,8 @@ function appendAuthOnboardingLines(lines, summary, prefix = "") {
73795
74022
  lines.push(`${prefix}Auth onboarding:`);
73796
74023
  if (!hasConfiguredPrivilegedPrincipal(summary)) {
73797
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`);
73798
- 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>\``);
73799
74027
  } else {
73800
74028
  lines.push(`${prefix} - inspect current app roles with: \`clisbot auth show app\``);
73801
74029
  }
@@ -73916,6 +74144,8 @@ function renderStartSummary(summary) {
73916
74144
  lines.push(...renderPairingSetupHelpLines(" ", {
73917
74145
  slackEnabled: summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled),
73918
74146
  telegramEnabled: summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled),
74147
+ ownerConfigured: hasConfiguredPrivilegedPrincipal(summary),
74148
+ ownerClaimWindowMinutes: summary.ownerSummary.ownerClaimWindowMinutes,
73919
74149
  slackDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "slack")?.directMessagesPolicy,
73920
74150
  telegramDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "telegram")?.directMessagesPolicy,
73921
74151
  conditionalOnly: true
@@ -73938,6 +74168,8 @@ function renderStartSummary(summary) {
73938
74168
  lines.push(...renderPairingSetupHelpLines("", {
73939
74169
  slackEnabled: summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled),
73940
74170
  telegramEnabled: summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled),
74171
+ ownerConfigured: hasConfiguredPrivilegedPrincipal(summary),
74172
+ ownerClaimWindowMinutes: summary.ownerSummary.ownerClaimWindowMinutes,
73941
74173
  slackDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "slack")?.directMessagesPolicy,
73942
74174
  telegramDirectMessagesPolicy: summary.channelSummaries.find((channel) => channel.channel === "telegram")?.directMessagesPolicy,
73943
74175
  conditionalOnly: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.20",
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",