garrytan/gstack · error · Error

Chain rejected: subcommand "${c.rawName}" not allowed by you

Error message

Chain rejected: subcommand "${c.rawName}" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). All subcommands must be within scope.

What it means

Before executing any subcommand, `chain` pre-validates EVERY entry against the caller's token scope using the canonical command name (lines 629-640). If the token is non-root and a subcommand is not in `tokenInfo.scopes`, the whole chain is rejected up front — no partial execution. This prevents privilege escalation by chaining an out-of-scope command behind an in-scope one.

Source

Thrown at browse/src/meta-commands.ts:634

      }

      // Canonicalize aliases across the whole chain. Pair canonical name with the raw
      // input so result labels + error messages reflect what the user typed, but every
      // dispatch path (scope check, WRITE_COMMANDS.has, watch blocking, handler lookup)
      // uses the canonical name. Otherwise `chain '[["setcontent","/tmp/x.html"]]'`
      // bypasses prevalidation or runs under the wrong command set.
      const commands = rawCommands.map(cmd => {
        const [rawName, ...cmdArgs] = cmd;
        const name = canonicalizeCommand(rawName);
        return { rawName, name, args: cmdArgs };
      });

      // Pre-validate ALL subcommands against the token's scope before executing any.
      // Uses canonical name so aliases don't bypass scope checks.
      if (tokenInfo && tokenInfo.clientId !== 'root') {
        for (const c of commands) {
          if (!checkScope(tokenInfo, c.name)) {
            throw new Error(
              `Chain rejected: subcommand "${c.rawName}" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). ` +
              `All subcommands must be within scope.`
            );
          }
        }
      }

      // Route each subcommand through handleCommandInternal for full security:
      // scope, domain, tab ownership, content wrapping — all enforced per subcommand.
      // Chain-specific options: skip rate check (chain = 1 request), skip activity
      // events (chain emits 1 event), increment chain depth (recursion guard).
      const executeCmd = opts?.executeCommand;
      const results: string[] = [];
      let lastWasWrite = false;

      if (executeCmd) {
        // Full security pipeline via handleCommandInternal.
        // Pass rawName so the server's own canonicalization is a no-op (already canonical).

View on GitHub (pinned to 94993f7401)

Solutions

  1. Inspect the token's scopes (the message lists them) and remove subcommands not present.
  2. Re-issue the token with the missing scope if the action is intended.
  3. Use a `root` token only if scope isolation is not required.
  4. Verify the canonical name — aliases are normalized before the scope check, so an alias for an out-of-scope canonical command still fails.

Example fix

// before: token scope = ["read"]
browse chain '[["goto","https://x"],["click","@e5"]]' // click not in scope
// after
browse chain '[["goto","https://x"],["text"]]' // text is in scope
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check every subcommand against the token's scope before sending.
const scopes = new Set(tokenInfo.scopes);
const disallowed = chainCommands.filter(c => !scopes.has(canonicalizeCommand(c)));
if (disallowed.length && tokenInfo.clientId !== 'root') {
  throw new Error(`Subcommands out of scope: ${disallowed.join(', ')}`);
}

Type guard

const isInScope = (token: TokenInfo | undefined, name: string): boolean =>
  !token || token.clientId === 'root' || token.scopes.includes(name);

Try / catch

try { await browse.chain(payload, { tokenInfo }); }
catch (err) {
  if (/Chain rejected/.test(err.message)) {
    // parse the listed scopes, filter the chain, and retry
  }
}

Prevention

When it happens

Trigger: A scoped API token (clientId !== 'root') issues `browse chain '[["click","@e5"]]` where `click` is not listed in the token's scopes (line 633). The check runs before any subcommand executes.

Common situations: A read-only token attempting a write subcommand (click/type/fill) inside a chain; an alias the user thought was in scope but was canonicalized to a different name; multi-tenant deployments where scopes differ per client.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/d63f28c831b70ffe. Report an issue: GitHub.