feat(setup): operator role prompt per channel, owner by default
Previously init-first-agent auto-granted global owner to the first
user wired through it and left every subsequent user as nothing — no
role, no membership. That worked for the bootstrap path but broke the
second channel's welcome DM: the access gate saw no role + no
membership and dropped the message with accessReason='not_member'.
Make the role explicit:
- scripts/init-first-agent.ts accepts --role owner|admin|member
(default: owner). Role drives the grant:
owner -> global owner (agent_group_id=null)
admin -> admin scoped to this agent group
member -> no role row, just membership
Idempotent via getUserRoles pre-check — safe on re-runs. addMember
runs unconditionally (INSERT OR IGNORE) so the access gate has a
row even for users who'd otherwise pass via role alone.
- setup/lib/role-prompt.ts — shared askOperatorRole(channel) prompt
with owner as the default pick. Self-host single-operator is the
dominant case, so the user's fingers default to Enter.
- Telegram / Discord / WhatsApp drivers all call askOperatorRole
before resolving the agent name and pass --role <choice> through.
Captured in progression log via setupLog.userInput('<channel>_role').
Summary output drops the fragile "promoted on first owner" hint in
favor of a dedicated role: line ("owner (global)" / "admin (scoped to
<ag-id>)" / "member") so re-runs make the current grant legible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { confirmThenOpen } from '../lib/browser.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
|
||||
|
||||
const DEFAULT_AGENT_NAME = 'Nano';
|
||||
@@ -88,6 +89,9 @@ export async function runDiscordChannel(displayName: string): Promise<void> {
|
||||
const dmChannelId = await openDmChannel(token, ownerUserId);
|
||||
const platformId = `discord:@me:${dmChannelId}`;
|
||||
|
||||
const role = await askOperatorRole('Discord');
|
||||
setupLog.userInput('discord_role', role);
|
||||
|
||||
const agentName = await resolveAgentName();
|
||||
|
||||
const init = await runQuietChild(
|
||||
@@ -100,6 +104,7 @@ export async function runDiscordChannel(displayName: string): Promise<void> {
|
||||
'--platform-id', platformId,
|
||||
'--display-name', displayName,
|
||||
'--agent-name', agentName,
|
||||
'--role', role,
|
||||
],
|
||||
{
|
||||
running: `Connecting ${agentName} to your Discord DMs…`,
|
||||
|
||||
@@ -22,6 +22,7 @@ import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { confirmThenOpen } from '../lib/browser.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import {
|
||||
type Block,
|
||||
type StepResult,
|
||||
@@ -96,6 +97,9 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const role = await askOperatorRole('Telegram');
|
||||
setupLog.userInput('telegram_role', role);
|
||||
|
||||
const agentName = await resolveAgentName();
|
||||
|
||||
const init = await runQuietChild(
|
||||
@@ -108,6 +112,7 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
'--platform-id', platformId,
|
||||
'--display-name', displayName,
|
||||
'--agent-name', agentName,
|
||||
'--role', role,
|
||||
],
|
||||
{
|
||||
running: `Connecting ${agentName} to your Telegram chat…`,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
spawnStep,
|
||||
writeStepEntry,
|
||||
} from '../lib/runner.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import { brandBold } from '../lib/theme.js';
|
||||
|
||||
const DEFAULT_AGENT_NAME = 'Nano';
|
||||
@@ -101,6 +102,9 @@ export async function runWhatsAppChannel(displayName: string): Promise<void> {
|
||||
writeAssistantHasOwnNumber();
|
||||
}
|
||||
|
||||
const role = await askOperatorRole('WhatsApp');
|
||||
setupLog.userInput('whatsapp_role', role);
|
||||
|
||||
const agentName = await resolveAgentName();
|
||||
|
||||
const platformId = `${chatPhone}@s.whatsapp.net`;
|
||||
@@ -115,6 +119,7 @@ export async function runWhatsAppChannel(displayName: string): Promise<void> {
|
||||
'--platform-id', platformId,
|
||||
'--display-name', displayName,
|
||||
'--agent-name', agentName,
|
||||
'--role', role,
|
||||
],
|
||||
{
|
||||
running: `Connecting ${agentName} to WhatsApp…`,
|
||||
@@ -128,6 +133,7 @@ export async function runWhatsAppChannel(displayName: string): Promise<void> {
|
||||
AGENT_NAME: agentName,
|
||||
PLATFORM_ID: platformId,
|
||||
MODE: isDedicated ? 'dedicated' : 'shared',
|
||||
ROLE: role,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
44
setup/lib/role-prompt.ts
Normal file
44
setup/lib/role-prompt.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Shared "who's connecting this channel?" prompt used by the channel setup
|
||||
* drivers before they hand off to scripts/init-first-agent.ts.
|
||||
*
|
||||
* Default: owner. Self-hosted NanoClaw is almost always a single-operator
|
||||
* deployment, and granting the same human owner status on every channel
|
||||
* they wire up matches what you'd want 99% of the time. The prompt
|
||||
* surfaces admin/member for the edge cases (shared instance, collaborators
|
||||
* with limited access), but hitting Enter assigns owner.
|
||||
*/
|
||||
import * as p from '@clack/prompts';
|
||||
|
||||
import { ensureAnswer } from './runner.js';
|
||||
|
||||
export type OperatorRole = 'owner' | 'admin' | 'member';
|
||||
|
||||
export async function askOperatorRole(
|
||||
channelLabel: string,
|
||||
): Promise<OperatorRole> {
|
||||
const choice = ensureAnswer(
|
||||
await p.select({
|
||||
message: `How should this ${channelLabel} account be registered?`,
|
||||
initialValue: 'owner',
|
||||
options: [
|
||||
{
|
||||
value: 'owner',
|
||||
label: 'Owner',
|
||||
hint: 'full access — recommended for your own account',
|
||||
},
|
||||
{
|
||||
value: 'admin',
|
||||
label: 'Admin',
|
||||
hint: 'can manage the agent for this channel',
|
||||
},
|
||||
{
|
||||
value: 'member',
|
||||
label: 'Member',
|
||||
hint: 'can chat with the agent but nothing more',
|
||||
},
|
||||
],
|
||||
}),
|
||||
) as OperatorRole;
|
||||
return choice;
|
||||
}
|
||||
Reference in New Issue
Block a user