{"record":{"id":"d63f28c831b70ffe","repo":"garrytan/gstack","slug":"chain-rejected-subcommand-c-rawname-not-allo","errorCode":null,"errorMessage":"Chain rejected: subcommand \"${c.rawName}\" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). All subcommands must be within scope.","messagePattern":"Chain rejected: subcommand \"(.+?)\" not allowed by your token scope \\((.+?)\\)\\. All subcommands must be within scope\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/meta-commands.ts","lineNumber":634,"sourceCode":"      }\n\n      // Canonicalize aliases across the whole chain. Pair canonical name with the raw\n      // input so result labels + error messages reflect what the user typed, but every\n      // dispatch path (scope check, WRITE_COMMANDS.has, watch blocking, handler lookup)\n      // uses the canonical name. Otherwise `chain '[[\"setcontent\",\"/tmp/x.html\"]]'`\n      // bypasses prevalidation or runs under the wrong command set.\n      const commands = rawCommands.map(cmd => {\n        const [rawName, ...cmdArgs] = cmd;\n        const name = canonicalizeCommand(rawName);\n        return { rawName, name, args: cmdArgs };\n      });\n\n      // Pre-validate ALL subcommands against the token's scope before executing any.\n      // Uses canonical name so aliases don't bypass scope checks.\n      if (tokenInfo && tokenInfo.clientId !== 'root') {\n        for (const c of commands) {\n          if (!checkScope(tokenInfo, c.name)) {\n            throw new Error(\n              `Chain rejected: subcommand \"${c.rawName}\" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). ` +\n              `All subcommands must be within scope.`\n            );\n          }\n        }\n      }\n\n      // Route each subcommand through handleCommandInternal for full security:\n      // scope, domain, tab ownership, content wrapping — all enforced per subcommand.\n      // Chain-specific options: skip rate check (chain = 1 request), skip activity\n      // events (chain emits 1 event), increment chain depth (recursion guard).\n      const executeCmd = opts?.executeCommand;\n      const results: string[] = [];\n      let lastWasWrite = false;\n\n      if (executeCmd) {\n        // Full security pipeline via handleCommandInternal.\n        // Pass rawName so the server's own canonicalization is a no-op (already canonical).","sourceCodeStart":616,"sourceCodeEnd":652,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/meta-commands.ts#L616-L652","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the token's scopes (the message lists them) and remove subcommands not present.","Re-issue the token with the missing scope if the action is intended.","Use a `root` token only if scope isolation is not required.","Verify the canonical name — aliases are normalized before the scope check, so an alias for an out-of-scope canonical command still fails."],"exampleFix":"// before: token scope = [\"read\"]\nbrowse chain '[[\"goto\",\"https://x\"],[\"click\",\"@e5\"]]' // click not in scope\n// after\nbrowse chain '[[\"goto\",\"https://x\"],[\"text\"]]' // text is in scope","handlingStrategy":"validation","validationCode":"// Pre-check every subcommand against the token's scope before sending.\nconst scopes = new Set(tokenInfo.scopes);\nconst disallowed = chainCommands.filter(c => !scopes.has(canonicalizeCommand(c)));\nif (disallowed.length && tokenInfo.clientId !== 'root') {\n  throw new Error(`Subcommands out of scope: ${disallowed.join(', ')}`);\n}","typeGuard":"const isInScope = (token: TokenInfo | undefined, name: string): boolean =>\n  !token || token.clientId === 'root' || token.scopes.includes(name);","tryCatchPattern":"try { await browse.chain(payload, { tokenInfo }); }\ncatch (err) {\n  if (/Chain rejected/.test(err.message)) {\n    // parse the listed scopes, filter the chain, and retry\n  }\n}","preventionTips":["Keep a manifest of which commands each token scope permits and validate client-side.","Canonicalize subcommand names before scope checks.","Issue tokens with the minimal scope set the chain actually needs."],"tags":["chain","security","authorization","token-scope"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}