nanocoai/nanoclaw · error · Error

owner role is always global (do not pass --group)

Error message

owner role is always global (do not pass --group)

What it means

Thrown by the `ncl roles grant` handler when the caller passes `--role owner` together with `--group`. NanoClaw's permission model treats the owner role as inherently global — it grants unrestricted access across all agent groups — so a group-scoped owner row is meaningless and the CLI rejects it before touching the `user_roles` table.

Source

Thrown at src/cli/resources/roles.ts:40

      description:
        'Null = global (all groups). A specific ID limits the role to that group. Owner must always be null.',
    },
    { name: 'granted_by', type: 'string', description: 'Who granted this role. Informational.' },
    { name: 'granted_at', type: 'string', description: 'Auto-set.' },
  ],
  operations: { list: 'open' },
  customOperations: {
    grant: {
      access: 'approval',
      description: 'Grant a role. Use --user, --role, and optionally --group for scoped admin.',
      handler: async (args) => {
        const userId = args.user as string;
        const role = args.role as string;
        const groupId = (args.group as string) ?? null;
        const grantedBy = (args.granted_by as string) ?? null;
        if (!userId) throw new Error('--user is required');
        if (!role || !['owner', 'admin'].includes(role)) throw new Error('--role must be owner or admin');
        if (role === 'owner' && groupId) throw new Error('owner role is always global (do not pass --group)');
        await getDb().run(
          `INSERT INTO user_roles (user_id, role, agent_group_id, granted_by, granted_at)
             VALUES (?, ?, ?, ?, ?)
             ON CONFLICT DO NOTHING`,
          userId,
          role,
          groupId,
          grantedBy,
          new Date().toISOString(),
        );
        return { user_id: userId, role, agent_group_id: groupId };
      },
    },
    revoke: {
      access: 'approval',
      description: 'Revoke a role. Use --user, --role, and --group if scoped.',
      handler: async (args) => {
        const userId = args.user as string;

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Re-run without the group flag: `ncl roles grant --user <id> --role owner`
  2. If a group-scoped privileged role is actually wanted, grant admin instead: `ncl roles grant --user <id> --role admin --group <agent-group-id>`
  3. In scripts, only include --group when role is admin

Example fix

# before
ncl roles grant --user telegram:alice --role owner --group grp_123
# after
ncl roles grant --user telegram:alice --role owner
Defensive patterns

Strategy: validation

Validate before calling

// before invoking: role=owner must not carry a group
const role = 'owner';
const flags = ['--user', userId, '--role', role];
if (group && role !== 'owner') flags.push('--group', group);
await ncl(['roles', 'grant', ...flags]);

Type guard

const isGrantableRoleCombo = (role: string, group?: string | null) =>
  (role === 'owner' || role === 'admin') && !(role === 'owner' && group);

Try / catch

catch (e) { if (e.message.includes('owner role is always global')) { retry without --group } else throw e; }

Prevention

When it happens

Trigger: Running `ncl roles grant --user <id> --role owner --group <agent-group-id>` (any non-empty --group value combined with role=owner). The check fires only in the grant verb, after the --user and --role validations pass.

Common situations: Operators copying an admin-grant command and changing the role to owner without dropping --group; scripting role grants generically so --group is always passed; migrating from a system where owners were scoped per workspace.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/11411809ef8b7c0c. Report an issue: GitHub.